/**
 * 数据管理与模版引擎
 *
 * @license MIT
 * Copyright (c) 2022 墨菲特
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 *  HISTORY:
 *
 *    @Author: Malphite
 *    @Date: 2022-04
 *    @description:
 *       1. 创建组件binder, 当前版本 v3.0.0
 *
 *    @Author: Malphite
 *    @Date: 2022-10
 *    @description:
 *    **********    组件重构  v4.0.0    **************
 *    一、取消对lodash的依赖
 *        通过重写一些lodash里面的方法，现在将这些方法进行内置加载。虽然达不到lodash那样好用但是勉强保持使用
 *    二、减少对jQuery的依赖
 *        大量的使用js原生的方式来操作dom，jQuery也不用额外添加，可以直接使用layui下面的内置jQuery。
 *        当前除了对接layui.form的部分几个方法和少部分方法，剩下的大部分方法不再使用jQuery。
 *    三、新增AST词法分析
 *        在v3版本的模版解析基于jQuery的contents、prop等方式，然后创建一个数组来保存相关细节。
 *        现在使用js原生的outerHTML或者使用自定义的template来进行词法分析，将分析结果以虚拟节点的形式保存到树中。
 *    四、优化了事件绑定
 *        在v3版本中采用观察者模式对事件进行绑定，出现过下面的问题
 *          1）、事件重复绑定，后来新增了一个shrink方法，用来手动清除。
 *          2）、绑定事件时属性必须存在，无法为后面可能出现的新数据动态的绑定事件，深监听效果差。
 *          3）、对于事件的绑定使用的时候过于的复杂，不够简洁。
 *        在v4版本使用aop的概念进行事件管理
 *          1）、在绑定事件是不需要考虑属性是否存在。
 *          2）、绑定事件时进行事件捕获，触发事件时进行事件冒泡，也可达到一个更好的深监听的效果
 *          3）、提供了一个简单的方法用于事件绑定
 *
 */
 ("use strict");
 (function (root, factory) {
   if (typeof exports === "object") {
     module.exports = factory();
   } else if (typeof define === "function" && define.amd) {
     define([], factory());
   } else if (layui && layui.define) {
     /**
      *
      */
     layui.define(["jquery"], function (exports) {
       // 初始化jQuery
       if (!window.$) window.$ = layui.$;
       // 判断是否引入了lodash
       if (!window._) window._ = buildLodash();
       // 声明其它组件
       importModule();
       // 导出组件
       exports("binder", factory());
     });
   } else {
     root.binder = factory();
   }
 })(this, function () {
   /**
    * 定义一系列的公共常量
    */
   let constant = {
     /**
      * @var {String} 当前组件版本号
      */
     version: "v4.0.0",
 
     /**
      * @var {String} 当前组件简称 b
      */
     mark: "b",
 
     /**
      * @var {String} 字符串分隔符 -> .
      * @description
      *    主要用于拆分属性key的时候使用
      */
     separator: ".",
 
     /**
      * @var {String} 数组前缀标志 -> [
      * @description
      *    主要用于拆分属性key的时候使用，数组是用[] 里面加下标来调用的
      */
     ARRAY_SEPARATOR_PREFIX: "[",
 
     /**
      * @var {String} 数组后缀标志 -> ]
      * @description
      *    主要用于拆分属性key的时候使用，数组是用[] 里面加下标来调用的
      */
     ARRAY_SEPARATOR_SUFFIX: "]",
 
     /**
      * @var {Number} 事件触发序列号，触发一次后进行自增
      */
     EVENT_SERIAL: 1,
 
     /**
      * @var {String} 属性值调用之前的回调函数名称
      */
     BEFORE_GET_KEY: "beforeGet",
 
     /**
      * @var {String} 属性值修改之前的回调函数名称
      */
     BEFORE_SET_KEY: "beforeUpdate",
 
     /**
      * @var {String} 属性值修改之后的回调函数名称
      */
     SETTED_KEY: "updated",
 
     /**
      * @var {String} 创建之前的的回调函数名称
      */
     BEFORE_CREATE_KEY: "beforeCreate",
 
     /**
      * @var {String} 创建之后的的回调函数名称
      */
     CREATED_KEY: "created",
 
     /**
      * @var {String} 销毁之前的的回调函数名称
      */
     BEFORE_DESTROY_KEY: "beforeDestroy",
 
     /**
      * @var {String} 销毁之后的的回调函数名称
      */
     DESTROYED_KEY: "destroyed",
 
     /**
      * @var {String} 挂载之前的的回调函数名称
      */
     BEFORE_MOUNT_KEY: "beforeMount",
 
     /**
      * @var {String} 挂载之后的的回调函数名称
      */
     MOUNTED_KEY: "mounted",
 
     /**
      * @var {String} layui表单验证 是否懒校验的属性值
      * @desc
      *    不设置这个属性 input采用 input onporpertychange 事件监听
      *    设置这个属性值 input采用 onbulr 事件监听
      *    当这个值设置为true的时候，属性值修改时不进行校验，在表单提交时通过方法一起校验
      *    当这个值设置为false or 其它时，在属性值发生改变时校验
      */
     LAYUI_LAZY: "lay-lazy",
 
     /**
      * @var {String} layui表单验证的属性值
      */
     LAYUI_VERIFY: "lay-verify",
 
     /**
      * @var {String} layui表单形式的属性值
      */
     LAYUI_VERIFY_TYPE: "lay-verType",
 
     /**
      * @var {String} layui表单验证提示信息的属性值
      */
     LAYUI_VERIFY_TEXT: "lay-reqText",
 
     /**
      * @var {String} layui表单验证失败提醒的class
      */
     LAYUI_DANGER: "layui-form-danger",
   };
 
   /**
    * 存放一些公共的变量
    */
   var POOL = {
     // 公共的指令
     COMMON_DIRETIVES: {},
     // 公共的方法
     COMMON_METHODS: {},
     // 公共的组件
     COMMON_EXTENDS: {},
   };
 
   /**
    * 部分工具方法集合
    */
   const util = {
     /**
      * @method 遍历整个字符串，每次回调会裁掉一段分隔片段，主要用于事件冒泡
      * @public
      * @desc  如果传入是一个字符串就进行字符串的拆分，就每次减少一次关于分隔符拆分的碎片
      *
      * @param {*} s   待处理资源 String
      * @param {*} fn  回调函数
      *        v         当前处理值
      *        source    待处理资源
      */
     bubble(s, fn) {
       if (_.isString(s)) {
         let res = s;
         do {
           let k = res.lastIndexOf(constant.separator);
           if (k >= 0) res = res.substring(0, k);
           _.isFunction(fn) && fn.call(this, res, s);
         } while (res.indexOf(constant.separator) > 0);
       }
     },
 
     /**
      * @method 获取vm里面的值
      * @scope  vm实例
      * @public
      * @desc
      *
      *    通过属性值对应的 key ，去vm里面取得所对应的值。取不到就返回空 null
      *
      * @param {*} k  属性值对应的key，比如 a.b[c]
      * @param {*} v  属性值，如果需要替换属性值，这个参数需要传入
      * @return 结果或null
      */
     getValue(k, v) {
       let [currentContext, flag, parent, key] = [this, true];
       _.every(k.split(constant.separator), (v1) => {
         let r = currentContext[v1];
         parent = currentContext;
         key = v1;
         // 数组特殊处理
         if (/\[\d+\]/.test(v1))
           r =
             currentContext[
               v1.substring(0, v1.indexOf(constant.ARRAY_SEPARATOR_PREFIX))
             ][
               v1.substring(
                 v1.indexOf(constant.ARRAY_SEPARATOR_PREFIX) + 1,
                 v1.indexOf(constant.ARRAY_SEPARATOR_SUFFIX)
               )
             ];
         if (r === undefined) {
           flag = false;
           return false;
         }
         currentContext = r;
       });
       // 赋值
       if (flag && v !== undefined) {
         parent[key] = v;
         return v;
       }
       return flag ? currentContext : null;
     },
 
     /**
      * @method 获取vm里面的值
      * @scope  vm实例
      * @public
      * @desc
      *
      *    补充方法，用于获取vDom里面有预置参数的情况
      *    通过属性值对应的 key ，去vm里面取得所对应的值。取不到就返回空 null
      *
      * @param {*} k  属性值对应的key，比如 a.b[c]
      * @param {*} v  属性值，如果需要替换属性值，这个参数需要传入
      * @param {*} m  vDom的mark
      * @param {*} a  vDom的arg
      * @return 结果或null
      */
     getMarkValue(k, v, m, a) {
       if (!k.startsWith(m + constant.separator))
         return util.getValue.call(this, k, v);
       let [currentContext, flag, parent, key] = [a, true];
       k = k.substring(m.length + 1, k.length);
       _.every(k.split(constant.separator), (v1) => {
         let r = currentContext[v1];
         parent = currentContext;
         key = v1;
         if (/\[\d+\]/.test(v1))
           r =
             currentContext[
               v1.substring(0, v1.indexOf(constant.ARRAY_SEPARATOR_PREFIX))
             ][
               v1.substring(
                 v1.indexOf(constant.ARRAY_SEPARATOR_PREFIX) + 1,
                 v1.indexOf(constant.ARRAY_SEPARATOR_SUFFIX)
               )
             ];
         if (r === undefined) {
           flag = false;
           return false;
         }
         currentContext = r;
       });
       // 赋值
       if (flag && v !== undefined) {
         parent[key] = v;
         return v;
       }
       return flag ? currentContext : null;
     },
 
     /**
      * @method 传入一段模版字符串,获取vm里面的值,得出结果
      * @scope  vm实例
      * @public
      * @desc
      *
      *    针对一般的模版字符串，可以尝试通过替换取值的方式得出最终的值,
      *    它还有个配套的方法 {@linkplain util.replace 替换每一个查找结果}
      *
      * @param {*} s 模版字符串
      * @return 结果
      */
     replaceAll(s) {
       let self = this;
       return String(s).replace(/({{[^{}]+}})/g, (c) =>
         util.replace.call(self, _.trim(c.substring(2, c.length - 2)))
       );
     },
 
     /**
      * @method 传入一段模版字符串,获取vm里面的值,得出结果
      * @private
      * @desc
      *    调用方法 {@linkplain util.getValue 获取vm里面对应的值}
      *
      *    是  {@linkplain util.replaceAll 这个方法的配套方法}
      * @param {*} s 模版字符串
      * @return 结果
      */
     replace(s) {
       if (s.indexOf("{{") >= 0) return util.replaceAll.call(this, s);
       return util.getValue.call(this, s) || "";
     },
 
     /**
      * @method 传入一段模版字符串,获取vm里面的值,得出结果
      * @scope  vm实例
      * @desc
      *
      *    一般的，针对复杂的模版字符串，可以尝试通过封装成函数的方式计算得出结果
      *
      * @param {*} s 模版字符串
      * @param {*} mark 临时变量
      * @param {*} arg 临时变量值
      * @return 结果
      */
     executeValue(s, mark = "b", arg = "b") {
       // 去掉前后空格
       s = _.trim(s);
       let self = this;
       const self_pre = "this";
       const return_pre = "return";
       let prefix =
         s.startsWith(self_pre + constant.separator) ||
         new RegExp("^" + mark + "\\" + constant.separator).test(s)
           ? " "
           : self_pre + constant.separator;
 
       // 模版字符串替换 {{ xxx }}  -> d.xxx
       if (s.indexOf("{{") >= 0) {
         s = s.replace(/({{[^{}]+}})/g, (c) => {
           let __str__ = _.trim(c.substring(2, c.length - 2));
           try {
             return new Function(
               mark,
               constant.mark,
               return_pre + " " + prefix + __str__
             ).call(self, arg, self);
           } catch (e) {
             if (
               e.name == "ReferenceError" ||
               e.name == "TypeError" ||
               e.name == "SyntaxError"
             ) {
               try {
                 return new Function(
                   mark,
                   constant.mark,
                   return_pre + " " + __str__
                 ).call(self, arg, self);
               } catch (error) {
                 console.warn("创建并调用函数出错!template:" + s, error);
                 return null;
               }
             } else {
               console.warn("创建并调用函数出错!template:" + s, e);
               return null;
             }
           }
         });
       } else {
         try {
           s = new Function(
             mark,
             constant.mark,
             return_pre + " " + prefix + s
           ).call(self, arg, self);
         } catch (e) {
           if (
             e.name == "ReferenceError" ||
             e.name == "TypeError" ||
             e.name == "SyntaxError"
           ) {
             try {
               s = new Function(mark, constant.mark, return_pre + " " + s).call(
                 self,
                 arg,
                 self
               );
             } catch (error) {
               console.warn("创建并调用函数出错!template:" + s, error);
               return null;
             }
           } else {
             console.warn("创建并调用函数出错!template:" + s, e);
             return null;
           }
         }
       }
 
       return s;
     },
 
     /**
      * 监视某个数据
      * @param {*} o
      *  target: 准备监视的数据
      *  key: 属性的唯一标志字符串, 比如:a.b.c[d]
      *
      * @return 已经被封装监视的对象
      */
     observe(o) {
       let self = this;
       // 数组对象特殊处理
       if (_.isArray(o.target)) {
         o.target.__self__ = self;
         o.target.__key__ = o.key;
         o.target.__proto__ = arrayMethods;
         _.each(o.target, (v, k) => {
           // let k1 =
           //   constant.ARRAY_SEPARATOR_PREFIX +
           //   k +
           //   constant.ARRAY_SEPARATOR_SUFFIX;
           // v = util.observe.call(self, {
           //   target: v,
           //   key: o.key ? o.key + constant.separator + k : k,
           // });
 
           // 2022/11/10 修改一下，不然list里面放字符串等就达不到那种效果了
           v = util.doObserve.call(self, {
             target: o.target,
             value: v,
             name: k,
             key: o.key ? o.key : k,
             // key: o.key ? o.key + constant.separator + k : k,
           });
         });
         return o.target;
       }
       if (!o.target || !_.isObject(o.target)) return o.target;
       // 普通对象的处理
       _.each(o.target, (v, k) => {
         util.doObserve.call(self, {
           target: o.target,
           value: v,
           name: k,
           key: o.key,
         });
       });
       return o.target;
     },
 
     /**
      * 响应式属性添加:
      *  将 value属性 加到 target 下面
      * @param {*} o
      *  target: 属性被添加的目标
      *  value:  待添加的属性值
      *  name：  待添加的属性对应目标的key
      *  key:    当前属性的唯一索引值 比如: a.b.c[d]
      *
      */
     doObserve(o) {
       // 根据传入的属性name，生成全新的索引值
       let self = this,
         fKey = o.key ? o.key + constant.separator + o.name : o.name;
       // 创建代理对象,将待监听的对象也创建为响应式数据
       let _proxy = util.observe.call(self, {
         target: o.value,
         key: fKey,
       });
       // 创建事件描述对象,也就是新建AOP切面
       self.proxy.dispatcher.add(fKey);
       // 调用Object.defineProperty
       Object.defineProperty(o.target, o.name, {
         configurable: true,
         enumerable: true,
         get() {
           // 调用事件描述对象的get回调参数
           self.proxy.dispatcher.get.call(self, fKey, _proxy, o.name);
           // 执行生命周期函数
           self[constant.BEFORE_GET_KEY] &&
             self[constant.BEFORE_GET_KEY].apply(self);
           return _proxy;
         },
         set(value) {
           // 排除 '' == 0 带来的影响，这个采用 ===
           if (value === _proxy) return;
           let oldValue = _proxy;
           // 调用事件描述对象的before回调参数
           self.proxy.dispatcher.before.call(
             self,
             fKey,
             value,
             oldValue,
             o.name
           );
           // 执行生命周期函数
           self[constant.BEFORE_SET_KEY] &&
             self[constant.BEFORE_SET_KEY].apply(self);
           _proxy = util.observe.call(self, {
             target: value,
             key: fKey,
           });
           // 执行生命周期函数
           self[constant.SETTED_KEY] && self[constant.SETTED_KEY].apply(self);
           // 调用事件描述对象的after回调参数
           self.proxy.dispatcher.after.call(self, fKey, value, oldValue, o.name);
         },
       });
     },
   };
 
   /**
    * @constructor  代理对象
    * 创建一系列临时对象，哪些不方便直接存放在vm下面的
    */
   var proxy = function () {
     let self = this;
 
     // 创建一个事件监听管理分发对象
     this.dispatcher = new AOP.Dispatcher();
 
     /**
      * @method 查询vm下面的一个属性
      * @desc
      *      这个方法的this 指向 vm
      * @param {*} k  属性的唯一key
      * @param {*} v  属性对应的替换值，可以为空
      * @returns vm 里面对应的值
      */
     this.getValue = function (k, v) {
       return util.getValue.call(this, k, v);
     };
 
     /**
      * @method 补充方法 查询vm下面的一个属性(当vDom里面包含预置参数时)
      * @param {*} k 属性的唯一key
      * @param {*} v 属性对应的替换值，可以为空
      * @param {*} m vDom的mark
      * @param {*} a vDom的arg
      * @returns
      */
     this.getMarkValue = function (k, v, m, a) {
       return util.getMarkValue.call(this, k, v, m, a);
     };
 
     /**
      * @method 查询vm下面的一个计算值
      * @desc
      *      这个方法的this 指向 vm
      *
      * @param {*} k 模版字符串
      */
     this.getTemplateValue = function (k) {
       return util.replaceAll.call(this, k);
     };
 
     /**
      * @method 传入一段模版字符串,获取vm里面的值,得出结果
      * @scope  vm实例
      * @desc
      *
      *    一般的，针对复杂的模版字符串，可以尝试通过封装成函数的方式计算得出结果
      *
      * @param {*} s 模版字符串
      * @param {*} mark 临时变量
      * @param {*} arg 临时变量值
      * @return 结果
      */
     this.executeValue = function (s, mark, arg) {
       return util.executeValue.call(this, s, mark, arg);
     };
 
     /**
      * 添加事件监听
      * @desc
      *      这个方法的this 指向 vm
      * @param {*} fn 待执行的事件，也是监听改变时调用的事件，如果是函数，就需要率先封装好
      */
     this.addListener = function (fn) {
       let res = null,
         serial = constant.EVENT_SERIAL++;
       const f = _.isFunction(fn);
       // 开启事件监听监测
       if (f) this.proxy.dispatcher.enableAccess.push(serial);
       res = f ? fn.apply(this) : util.getValue.call(this, fn);
       // 添加事件监听
       if (f) this.proxy.dispatcher.addListener(fn, false, serial);
       return res;
     };
 
     /**
      * @public
      * @method 添加属性
      * @param {*} obj 待添加的对象 or 执行函数
      * @param {*} k   添加属性的key
      * @param {*} v   添加属性的值
      * @desc
      *    简单的，在属性赋值之后将vm里面的data重新解析一遍
      */
     this.set = function (obj, k, v) {
       let self = this;
       if (_.isFunction(obj)) {
         obj.apply(self);
       } else {
         let o = {};
         o[k] = v;
         obj = Object.assign(obj, o);
       }
       self.$data = util.observe.call(self, { target: self.$data });
       Object.keys(self.$data).forEach((key) => {
         Object.defineProperty(self, key, {
           configurable: true,
           enumerable: true,
           get() {
             return self.$data[key];
           },
           set(value) {
             if (value == self.$data[key]) return;
             self.$data[key] = value;
           },
         });
       });
     };
 
     /**
      * @public
      * @method 获取虚拟属性当前值
      * @desc
      *    this 指向vm
      *    这个是在解析虚拟节点里面的属性值时被调用
      *    特意把这个方法提取出来，供外部和内部的指令使用
      *
      *    1. else可能会去找if的属性值进行判断，需要给它提供一个解析的方法
      *    2. 其它指令或许会更改一些属性，将这个方法提供出去方便修改
      *
      * @param {*} attr  虚拟属性对象
      * @param {*} vDom  虚拟节点对象
      * @returns  虚拟属性当前值
      */
     this.parseAttr = function (attr, vDom) {
       let flag = !!vDom.mark;
       if (flag && new RegExp(vDom.mark).test(attr.expression)) {
         // 如果是含临时变量，而且表达式里面有临时变量的表达式，就构建一个函数去获取值
         return this.proxy.executeValue.call(
           this,
           attr.expression,
           vDom.mark,
           vDom.arg
         );
       } else if (/{{[\s\S]*}}/.test(attr.expression)) {
         // 带有模版字符串
         return this.proxy.getTemplateValue.call(this, attr.expression);
       } else if (/[!=<>?:%*/+-]/.test(attr.expression)) {
         // 2022/11/10 新增，如果里面含有 !=<>?:%*/+-等计算符号就算做第一类
         return this.proxy.executeValue.call(this, attr.expression);
       } else {
         // 普通的字符串
         return this.proxy.getValue.call(this, attr.expression);
       }
     };
 
     // 初始化生命周期函数
     this.initEvents = function (option = {}) {
       this[constant.BEFORE_CREATE_KEY] = option[constant.BEFORE_CREATE_KEY];
       this[constant.CREATED_KEY] = option[constant.CREATED_KEY];
       this[constant.BEFORE_DESTROY_KEY] = option[constant.BEFORE_DESTROY_KEY];
       this[constant.DESTROYED_KEY] = option[constant.DESTROYED_KEY];
       this[constant.BEFORE_MOUNT_KEY] = option[constant.BEFORE_MOUNT_KEY];
       this[constant.MOUNTED_KEY] = option[constant.MOUNTED_KEY];
       this[constant.BEFORE_GET_KEY] = option[constant.BEFORE_GET_KEY];
       this[constant.BEFORE_SET_KEY] = option[constant.BEFORE_SET_KEY];
       this[constant.SETTED_KEY] = option[constant.SETTED_KEY];
     };
 
     // 初始化数据 vm
     this.initData = function (dataOption = {}, dataSource = {}) {
       let self = this;
       let data = _.isFunction(dataOption)
         ? dataOption.call(self, dataSource)
         : dataOption;
       self.$data = util.observe.call(self, { target: data });
       Object.keys(self.$data).forEach((key) => {
         Object.defineProperty(self, key, {
           configurable: true,
           enumerable: true,
           get() {
             return self.$data[key];
           },
           set(value) {
             if (value == self.$data[key]) return;
             self.$data[key] = value;
           },
         });
       });
     };
 
     // 初始化计算属性
     this.initComputed = function (computeds = {}) {
       let self = this,
         tempProxy = {};
       _.each(computeds, (v, k) => {
         let _getter = _.isFunction(v) ? v : v.get;
         let _setter =
           v.set ||
           function (s) {
             tempProxy[k] = s;
           };
         self.proxy.addListener.call(self, function () {
           /**
            * 仅在第一次初始化的时候添加defineProperty
            * 后面直接执行self[k] = 来触发 set里面的回调函数进行更新
            */
           if (tempProxy[k] !== undefined) {
             self[k] = _getter.apply(self);
             return;
           }
           tempProxy[k] = _getter.apply(self);
           self.proxy.dispatcher.add(k);
           Object.defineProperty(self, k, {
             configurable: true,
             enumerable: true,
             get() {
               self.proxy.dispatcher.get.call(self, k, tempProxy[k], k);
               return tempProxy[k];
             },
             set(value) {
               let oldValue = tempProxy[k];
               if (value == tempProxy[k]) return;
               self.proxy.dispatcher.before.call(self, k, value, oldValue, k);
               tempProxy[k] = value;
               _setter.call(self, value);
               self.proxy.dispatcher.after.call(self, k, value, oldValue, k);
             },
           });
         });
       });
     };
 
     /**
      * 初始化指令
      *
      */
     this.initDiretives = function (diretives = {}) {
       let self = this;
       if (!self.proxy.diretives) self.proxy.diretives = {};
       _.each(POOL.COMMON_DIRETIVES, (v, k) => (self.proxy.diretives[k] = v));
       _.each(diretives, (v1, k1) => {
         let _diretive = _.isFunction(v1)
           ? new RENDERER.directive(v1)
           : new RENDERER.directive(v1.bind, v1.update);
         self.proxy.diretives[k1] = _diretive;
       });
     };
 
     // 初始化组件
     this.initComponents = function (components = {}) {
       let self = this;
       self.proxy.components = {};
       _.each(components, (v1, k1) => {
         let k = k1.toLowerCase();
         if (_.isArray(v1) && v1.length == 2) {
           let component = self.proxy.getComponent.call(self, v1[0]);
           if (null != component) self.proxy.components[k] = [component, v1[1]];
         } else {
           let component = self.proxy.getComponent.call(self, v1);
           if (null != component) self.proxy.components[k] = component;
         }
       });
     };
 
     /**
      * 获取组件
      * @param {*} component
      * @returns
      */
     this.getComponent = function (component) {
       if (component instanceof Component) return component;
       if (_.isString(component)) return POOL.COMMON_EXTENDS[component];
       if (_.isObject(component)) return binder.extend(component);
       return null;
     };
 
     // 初始化方法
     this.initMethods = function (methods = {}) {
       let self = this;
       _.each(POOL.COMMON_METHODS, (v, k) => (self[k] = v));
       _.each(methods, (v1, k1) => (self[k1] = v1));
     };
 
     // 初始化监视属性
     this.initWatchs = function (watchs = {}) {
       let self = this;
       _.each(watchs, (v, k) => self.proxy.watch.call(self, k, v));
     };
 
     /**
      * @method 添加监视属性
      * @param {*} key  待监视的key
      * @param {*} fn   监视回调
      */
     this.watch = function (key, fn) {
       let self = this;
       if (_.isFunction(fn)) {
         self.proxy.dispatcher.watch(self, key, fn);
       } else {
         self.proxy.dispatcher.watch(
           self,
           key,
           fn.handler,
           fn.deep,
           fn.immediate
         );
       }
     };
 
     // 解析并上树
     this.mount = function (destination) {
       // 执行生命周期函数
       this[constant.BEFORE_MOUNT_KEY] &&
         this[constant.BEFORE_MOUNT_KEY].apply(this);
       if (_.isString(destination))
         destination = document.querySelector(destination);
       // flag == true  使用原始  从父节点插入
       // flag == false 清空插入  从当前节点插入
       const flag = this.template === undefined;
       let dom = AST.parse(flag ? destination.outerHTML : this.template)[0];
       let parentNode = flag ? destination.parentNode : destination;
       let pDom = new vDom(parentNode.tagName, parentNode.nodeType, this);
       pDom.ele = parentNode;
       if (flag) {
         parentNode.insertBefore(
           RENDERER.render.call(this, dom, pDom),
           destination
         );
         parentNode.removeChild(destination);
       } else {
         var childs = destination.childNodes;
         for (var i = childs.length - 1; i >= 0; i--) {
           destination.removeChild(childs[i]);
         }
         destination.appendChild(RENDERER.render.call(this, dom, pDom));
       }
       this.parent = parentNode;
       // 执行生命周期函数
       this[constant.MOUNTED_KEY] && this[constant.MOUNTED_KEY].apply(this);
     };
 
     // 将表单验证的3个方法提供出去，方便vform里面的调用，暂时只能这样。
     this.verifyItem = function (item) {
       return FormRender.verifyItem.call(this, item);
     };
 
     this.verifyForm = function (filter) {
       return FormRender.verifyForm.call(this, filter);
     };
 
     this.getFormValue = function (filter) {
       return FormRender.getFormValue.call(this, filter);
     };
   };
 
   /**
    * @namespace AOP
    * @desc
    *
    *    事件监听
    *
    * HISTORY:                                            *
    *     @Author: Malphite                                 *
    *     @Date: 2022-10-14
    *     @since: v4.0.0
    *
    * @desc
    *
    *    AOP事件管理模型:
    *
    *    首先每一个vm对象都会创建自己的{@linkplain proxy 代理对象}，每个代理对象也会创建自己的{@linkplain AOP.Dispatcher 事件分发器}
    * 在事件分发器里面维护了一个事件切面容器池coreMap，里面保存的是一个个的{@linkplain AOP.Aspect 事件切面}。 在一个切面里面又会有一个
    * 列表，里面维护了一个个的{@linkplain AOP.Event 事件监听对象}。待触发的函数就保存在事件监听对象的fn属性里面了。
    *
    *    {@linkplain util.observe 数据初始化} 在动态数据创建时会自动的为这个数据创建一个数据切面。(直接在vm里面一直找到分发器，由分发器新增一个数据切面)
    * 在数据的get和set方面中添加上分发器里面的通知。这里只有3类通知: get通知，在数据被调用时调用、 before通知，在数据被修改之前调用、 after通知，在数据被修改之后调用。
    *
    *    事件捕获过程:
    *
    *  接下来模拟一次事件捕获过程, 通过调用proxy对象提供的addListener(fn)方法可以将fn执行并加入触发函数中。
    *
    *    1）、 调用 proxy里面的 addListener(fn) 当前 this 指向 vm
    *      首先获取{@linkplain constant.EVENT_SERIAL 事件触发序列号},作为这次捕获的编号，在函数触发的时候可能会递归多次捕获事件，用这个序列号进行区分。
    *      将事件触发序列号放入分发器的序列号栈中，表示开始捕获
    *      执行fn方法(这里会触发下面 2)、3)  的过程用来捕获)
    *      最后调用分发器的addListener方法将fn放入已捕获切面中，序列号也是在这里从列表中移除  也就是过程 4）
    *    2）、在1）中执行fn时可能会从vm中获取参数值，这些参数就会被触发他们的get回调函数，也就会触发分发器里面的 get 通知了
    *    3）、执行分发器里面的 get通知回调函数，会去检查它里面的序列号栈的长度是否为0，如果不是，会调用当前切面的online方法捕获这个切面，并带上序列号。
    *    4）、在fn回调执行结束，分发器会检查所有的切面，查看他们是否被当前序列号捕获，如果是就在他们的事件列表里面将这个fn放进去。
    *
    *    事件冒泡过程:
    *
    *  回调事件的调用现在统一发生在after通知过程中
    *
    *    1）、 数据发生改变时(要和原来的值不一样)，时会触发它的set回调，也就会触发分发器里面的after通知了
    *    2）、 在after通知里面首先会去找到切面，然后执行它里面的事件监听列表，过滤掉返回值是false的事件监听对象
    *    3）、然后将它里面的key a.b.c 来进行事件冒泡(执行它父对象的切面回调函数)这个是基于字符串拆分的
    *
    */
   var AOP = {
     /**
      * @constructor  事件分发器
      */
     Dispatcher: function () {
       let self = this;
 
       /**
        * @var {Array} 序列号栈
        * 里面存放的是{@linkplain constant.EVENT_SERIAL 事件序列号}
        * 如果它的length是0，代表当前没有需要捕获的工作，否则它的最后一项代表当前捕获任务的序列号
        */
       this.enableAccess = [];
 
       /**
        * @var {*}  事件切面容器池
        * 存放 {@linkplain AOP.Aspect 事件切面} ,以属性的key为值，方面后面事件冒泡
        */
       this.coreMap = {};
 
       /**
        * @method 添加事件监听
        * @desc
        *    响应式触发原理:
        *      1.在响应式数据触发后，会带上某些标记，为这些已标记的数据添加监听回调函数
        *      2.在数据的setted回调函数里面去依次调用所有已加入的回调函数
        *
        * @param {*} fn    待加入的监听回调函数
        * @param {*} flag  是否深监听 true 是  false 否
        * @param {*} serial    当前捕获序列号
        * @param {*} cb    每次添加后执行的回调函数
        * @returns
        */
       this.addListener = (fn, flag = false, serial, cb) => {
         // 添加回调函数
         _.each(self.coreMap, (v) =>
           v.add(fn, flag ? "deep" : "bind", serial, cb)
         );
         // 移除事件序列号
         self.enableAccess.splice(self.enableAccess.indexOf(serial), 1);
         // 如果没有捕获任务了，将所有的切面释放
         if (self.enableAccess.length == 0)
           _.each(self.coreMap, (v) => v.offline());
       };
 
       /**
        * @method 添加监视属性
        * @desc
        *    这个方法绕过了响应式触发原理 {@linkplain AOP.Dispatcher.addListener 类似于这种形式}
        *  是直接向事件集合里面添加回调函数。
        *    这个方法可以用在创建vm的时候添加监视属性，和为vm添加一个监视属性
        *
        * @param {*} vm     指定vm，测试时发现这个函数用call改变this指向失效
        * @param {*} key    响应式数据的唯一key
        * @param {*} fn     触发回调函数
        * @param {*} deep   是否是深监视
        * @param {*} immediate  回调函数是否立即执行
        */
       this.watch = (vm, key, fn, deep = false, immediate = false) => {
         // 没有就添加一个
         if (!self.coreMap[key]) self.add(key);
         self.coreMap[key].add(fn, deep ? "deep" : "bind", null, null, true);
         // 需要立即执行
         if (immediate) fn.call(vm, util.getValue.call(vm, key), null, key);
       };
 
       /**
        * 添加一个事件切面
        * @param {*} key 属性的唯一key
        * @returns
        */
       this.add = (key) => {
         if (!self.coreMap[key]) self.coreMap[key] = new AOP.Aspect(key);
       };
 
       /**
        * 在一个属性值被调用前的回调函数
        * 这个里面的this 指向 vm
        * @param {*} key 该属性值的唯一key
        * @param {*} v   当前属性值
        * @param {*} k   当前属性的key(对应父属性)
        */
       this.get = function (key, v, k) {
         // 开启添加事件的监听
         self.enableAccess.length > 0 &&
           self.coreMap[key].online(self.enableAccess);
         // 执行vm默认的回调事件
         this[constant.BEFORE_GET_KEY] &&
           this[constant.BEFORE_GET_KEY].call(this, v, k, key);
       };
 
       /**
        * 在一个属性值被修改前的回调函数
        * 这个里面的this 指向 vm
        * @param {*} key  该属性值的唯一key
        * @param {*} v    当前属性值
        * @param {*} v1   属性旧值
        * @param {*} k    当前属性的key(对应父属性)
        */
       this.before = function (key, v, v1, k) {
         // 执行vm默认的回调事件
         this[constant.BEFORE_SET_KEY] &&
           this[constant.BEFORE_SET_KEY].call(this, v, v1, k, key);
       };
 
       /**
        * 在一个属性值被修改后的回调函数
        * 这个里面的this 指向 vm
        * @param {*} key  该属性值的唯一key
        * @param {*} v    当前属性值
        * @param {*} v1   属性旧值
        * @param {*} k    当前属性的key(对应父属性)
        */
       this.after = function (key, v, v1, k) {
         // 执行监听回调
         let vm = this;
         // 1. 执行直接监听回调,  如果回调函数返回值为 false 那么在执行之后会被移除列表
         self.coreMap[key].pool = _.filter(
           self.coreMap[key].pool,
           function (event) {
             return false !== event.fn.call(vm, v, v1, k, key);
           }
         );
         // 2.冒泡触发
         util.bubble(key, (v) => {
           if (v !== key && self.coreMap[v])
             _.each(
               self.coreMap[v].pool,
               (e) => "deep" === e.type && e.fn.call(vm, v, v1, k, key)
             );
         });
         // 执行vm默认的回调事件
         this[constant.SETTED_KEY] &&
           this[constant.SETTED_KEY].call(this, v, v1, k, key);
       };
     },
 
     /**
      * @constructor  事件切面
      */
     Aspect: function (key) {
       let self = this;
 
       /**
        * @var {Boolean} 捕获标志， true代表已捕获， false代表未捕获
        */
       this.access = false;
 
       /**
        * @var {*} 事件序列号列表，保存被捕获时的事件序列编号
        */
       this.serial = [];
 
       /**
        * @var {*} 事件监听对象集合
        */
       this.pool = [];
 
       /**
        * 当前切面的唯一编号
        */
       this.key = key;
 
       /**
        * @method 开启添加事件捕获
        * @param {*} serial 当前事件序列号
        * @desc
        *  1.将捕获标志设置为true
        *  2.如果事件序列号列表中没有该序列号，就将这个序列号加入
        */
       this.online = (serial) => {
         self.access = true;
         _.each(serial, (s) => {
           if (self.serial.indexOf(s) < 0) self.serial.push(s);
         });
       };
 
       /**
        * 关闭事件捕获，释放该切面
        */
       this.offline = () => {
         self.access = false;
         self.serial = [];
       };
 
       /**
        * 添加回调参数
        * @param {*} fn 回调函数
        * @param {*} type 回调类型 deep - 深监听
        * @param {*} serial 当前事件序列号
        * @param {*} cb 每次添加成功后执行的回调函数
        */
       this.add = (fn, type, serial, cb, flag) => {
         if (flag) return self.pool.push(new AOP.Event(fn, type));
         // 检查捕获标志
         if (!self.access) return false;
         // 传入了事件序列号并且这个序列号已经被捕获过，就新增一个事件监听对象
         // 没有传入事件序列号就直接新增一个事件监听对象
         if (!!serial && self.serial.indexOf(serial) >= 0) {
           self.pool.push(new AOP.Event(fn, type));
           cb && cb(self.key);
         } else if (!serial) {
           self.pool.push(new AOP.Event(fn, type));
           cb && cb(self.key);
         }
       };
     },
 
     /**
      * @constructor  事件监听对象
      * @param {*} fn 回调函数
      * @param {*} type 函数类型: 'bind', 'deep' 后面的类型在子属性冒泡阶段也会被触发
      */
     Event: function (fn, type = "bind") {
       this.fn = fn;
       this.type = type;
     },
   };
 
   /**
    * @namespace AST
    * @desc
    *    AST抽象语法树
    *
    * HISTORY:                                            *
    *     @Author: Malphite                                 *
    *     @Date: 2022-10-29
    *     @since: v4.0.0
    *
    * @desc
    *
    *    AST抽象语法树*结构:
    *
    *    1、首先解析的内容必须外层只有一个节点，没有节点标签均闭合。
    *    2、该树中将HTML标签节点提取成一个 {@linkplain vDom 虚拟dom节点} 简称虚拟节点。
    *    3、虚拟节点分为普通虚拟节点(nodeType = 1)和文本虚拟节点(nodeType = 3, tagName = TEXT)
    *    4、每个虚拟节点有下面的属性:tagName(HTMLElement的tagName属性), nodeType(普通节点-1、文本节点-3),
    * self(所属的vm,后面废弃了,传入的是一个空对象), attrs(属性集合), children(子节点虚拟节点)
    *    5、attrs是一个key-value键值对。key是属性的名称(=前面的值) value是一个 {@linkplain vAttr 虚拟属性节点} 简称虚拟属性
    *    6、每个虚拟属性有下面的属性 expression (原始表达式)、 currentValue (最新计算值)、 value (当前属性值)
    *
    *    vDom - children ---  vDom
    *                     |_  vDom
    *                     |_  vDom - children ---  vDom
    *                                          ....
    *
    *    调用AST.parse(template) 进行 {@linkplain AST.parse 模版解析}  , 将template转化为 AST抽象语法树* 。
    *
    *      1）、准备两个栈： {@linkplain AST.stack 结果栈} 和 {@linkplain AST.mark 标签栈} 将这两个栈清空。
    *      2）、给结果栈率先录入一个空数组进去，用于保存最后的结果，最后是将这个空数组弹出。
    *      3）、初始化指针 p = 0, 和 template的长度 len
    *      4）、循环，直到指针到 len
    *
    *          执行 {@linkplain AST.doParse 获取解析器，解析字符串}
    *          这个方法会返回一个字符串或者空字符串，如果是返回了字符串则指针p移动该字符串的长度，否则指针下移一位。
    *
    *      5）、弹出结果栈顶，并返回。它的第一项就是生成的 AST抽象语法树*
    *
    *    调用{@linkplain AST.getResolvers 获取所有的字符串解析器}，解析器会判断是否能被该解析器解析，如果被解析了会返回被解析的内容
    *    一个 {@linkplain AST.resolver 字符串解析器} 包括两个部分
    *  reg -- 正则表达式，用于判断是否可以被该解析器解析  action -- 解析方法, 接收一个待解析的字符串，返回一个已解析的字符串
    *
    *    {@linkplain MyResolvers 内置5种内容解析器}  基本满足了解析要求
    *
    */
   var AST = {
     /**
      * @var {*} 自闭合标签列表
      */
     CLOSE_MAP:
       "area、base、br、col、command、embed、hr、img、input、keygen、link、meta、param、source、track、wbr".split(
         "、"
       ),
 
     /**
      * @public
      * @method 模版解析
      * @param {*} t  待解析模版
      * @returns [AST抽象语法树*]
      */
     parse(t) {
       /**
        * 首先准备两个栈  AST.stack   AST.mark
        * 将两个栈清空， AST.stack栈中放入一个空数组来保存结果
        */
       AST.stack = [{}];
       if (!t) return AST.stack.pop().children;
       AST.mark = [];
       /**
        * 创建指针p,获取template长度 len
        */
       var p = 0,
         len = t.length;
       while (p < len) {
         /**
          * 调用 {@linkplain AST.doParse 解析字符串进行词法分析}
          */
         // 由于传入的是剩余字符串长度，所以下面的正则几乎是从头开始判断，但是不强制以啥子结尾。结尾都是这个字符串的结尾来着
         var s = AST.doParse(t.substring(p));
         // 根据返回结果来移动指针
         p += s ? s.length : 1;
       }
       return AST.stack.pop().children;
     },
     /**
      * 获取解析器，解析字符串
      * @param {*} s 待解析的字符串
      */
     doParse(s) {
       // 默认返回为空
       var res = "";
       /**
        * 获取所有的 {@linkplain AST.resolver 解析器}
        * 依次的进行遍历，如果传入的字符串和解析器里面的正则表达式匹配上了，就执行它的解析方法
        * 并返回它解析方法的返回值。
        * 一旦有一个解析器成功解析了，就跳过剩余的解析器
        */
       Array.prototype.every.call(AST.getResolvers(), function (r) {
         if (r.reg.test(s)) {
           res = r.action(s);
           return false;
         }
         return true;
       });
       return res;
     },
     /**
      * 字符串解析器
      * @constructs
      * @lends AST.resolver
      * @param {*} reg 正则表达式
      * @param {*} action 解析方法 参数是待解析的字符串 返回解析过的字符串
      */
     resolver: function (reg, action) {
       if (Object.prototype.toString.call(reg) !== "[object RegExp]") {
         action = reg.action;
         reg = reg.reg;
       }
       this.reg = reg;
       this.action = (...param) => action.apply(this, param);
     },
     /**
      * 获取全部的解析器
      * @returns  全部的解析器
      */
     getResolvers() {
       // 如果没有初始化就调用方法初始化
       if (!AST.resolvers) AST.initResolver();
       return AST.resolvers;
     },
 
     /**
      * 初始化几种解析器
      * 内置 5 种 解析器 {@link MyResolvers}
      */
     initResolver() {
       if (AST.resolvers) return AST.resolvers;
       AST.resolvers = [];
       AST.resolvers.push(MyResolvers.start);
       AST.resolvers.push(MyResolvers.end);
       AST.resolvers.push(MyResolvers.comments);
       AST.resolvers.push(MyResolvers.innertext);
       AST.resolvers.push(MyResolvers.outertext);
       return AST.resolvers;
     },
   };
 
   /**
    * @namespace MyResolvers
    * @desc
    *      保存内置的5种解析器:
    *
    *      1. {@linkplain MyResolvers.start 标签开始解析器} 它匹配的是标签的开头   <(XXX) YYY?/?>
    *      (禁用,在chrome上面测试，并没有自闭合标签的写法 <XXX /> 而是 <XXX >) 2. {@linkplain MyResolvers.closing 自闭合标签结束解析器} 它匹配的是自闭合标签的结束   />
    *      3. {@linkplain MyResolvers.end 标签结束解析器} 它匹配的是标签的闭合 </XXX>
    *      4. {@linkplain MyResolvers.comments 注释解析器} 它匹配的是html注释   <!-- -->
    *      5. {@linkplain MyResolvers.innertext 内文本解析器} 它匹配的是标签间内的文本 > XXXX </ZZZ
    *      6. {@linkplain MyResolvers.outertext 外文本解析器} 它匹配的是标签间外的文本 > XXXX <ZZZ  第一个 > 是没有的了
    *
    *    在实际测试中 2 情况没有发生,自闭合标签 input 最后获取为 <input> 而不是 <input/> 所以将2禁用，防止解析错误
    *   将{@linkplain AST.CLOSE_MAP 自闭合标签} 记录下来特殊处理。它的处理方式类似与文本，tagName不会计入 mark栈中，所以不能再去判断它的闭合
    */
   var MyResolvers = {
     /**
      * @method  解析器一: 解析开始标签 <(XXX) YYY?/?>
      * @desc
      *        1. 将 XXX 提取出来作为 tagName
      *        2. 将 XXX 压入 mark 栈
      *        3. 创建一个节点对象
      *        4. 解析属性值
      *        5. 将节点对象压入 stack 栈
      *        6. 返回
      *    这里返回的是 `<` + 标签 + 属性
      *    所以指针最后会指向 `>`
      *
      * 2022/11/02 添加自闭合标签的判断
      * 自闭合标签不会像预料中的 以 <XXX /> 来标志 在chrome10中解析成了 <XXX>
      * 所以处理成类似文本标签的解析方式，不进入栈中
      *
      * 2022/11/09 修改正则表达式,这样可以允许属性值里面出现 <>= 这样的关键字而不影响解析
      *
      */
     start: new AST.resolver({
       // reg: /^\s*<([A-z]+[0-9]?)(\s[^\<]*)?\/?>/,
       reg: /^\s*<([A-z]+[0-9]?)(\s(\s*[^=<>]*\-?[^=<>\s]+\s*(=\s*(\"[^\"]*\"|\'[^\']*\'))?\s*)*)?\s*\/?>/,
       action: function (s) {
         // 1.分组捕获出 XXX 标签名 和 YYY 属性字符串
         let matchRes = s.match(this.reg);
         let tag = matchRes[1];
         const flag = AST.CLOSE_MAP.indexOf(tag) < 0;
         // 2.压入 mark 栈
         if (flag) AST.mark.push(tag);
         // 3.创建普通虚拟节点对象
         let node = new vDom(tag, 1, {});
         // 4.解析属性值
         let attrs = matchRes[2] || "";
         if (attrs && !/^\s*$/.test(attrs)) {
           node.attrs = {};
           const attrRegExp =
             /[^=<>\s\/]*\-?[^=<>\s\/]+\s*(=\s*(\"[^\"]*\"|\'[^\']*\'))?/g;
           attrs.replaceAll(attrRegExp, (v) => {
             // 上面的正则决定了，最后这里只剩余一个 = ,所以使用它将key和value拆分开
             // 2022/11/09 这下 = 不是只有一个了，但是由于 indexOf是查找第一个 = 所以还是不影响
             // 考虑到无属性值到写法
             v = v.indexOf("=") < 0 ? v + '=""' : v;
             let p = v.indexOf("=");
             let key = v.substring(0, p).replaceAll(/^\s+|\s+$/g, "");
             let value = v.substring(p + 1).replaceAll(/^\s+|\s+$/g, "");
             // 这里捕获的value带有引号，需要去掉
             // 2022/11/09创建虚拟属性节点
             node.attrs[key] = new vAttr(value.substring(1, value.length - 1));
           });
         }
         if (flag) {
           AST.stack.push(node);
         } else {
           AST.stack[AST.stack.length - 1].children.push(node);
         }
         return s.substring(0, s.indexOf(tag) + tag.length + attrs.length);
       },
     }),
 
     /**
      * @method  解析器二: 解析闭合标签的结束 />
      * @desc
      *        1. 弹栈 mark 栈里面的栈顶弹出来，由于这个是自闭合标签，所以无需校验
      *        2. 弹栈 stack 栈里面的栈顶弹出来 node
      *        3. 将node 作为children放入 stack的栈顶
      *        4. 返回  />
      */
     closing: new AST.resolver({
       reg: /^\s*\/>/,
       action: function (s) {
         let matchRes = s.match(this.reg);
         // 1. 弹栈 mark 不校验
         AST.mark.pop();
         // 2. 弹栈 stack 弹出node对象
         let node = AST.stack.pop();
         // 3. 如果 stack 的栈顶没有 children 属性就添加一个空数组
         if (!AST.stack[AST.stack.length - 1].children)
           AST.stack[AST.stack.length - 1].children = [];
         // 将 node 作为children放入 stack的栈顶
         AST.stack[AST.stack.length - 1].children.push(node);
         return matchRes[0];
       },
     }),
 
     /**
      * @method  解析器三: 解析标签的闭合 </XXX>
      * @desc
      *        1. 将 XXX 提取出来作为 tagName
      *        2. 弹栈 mark 栈里面的栈顶弹出来，将 tagName 与结果进行校验，如果不一致就说明标签未闭合，报错
      *        3. 弹栈 stack 栈里面的栈顶弹出来 node
      *        4. 将node 作为children放入 stack的栈顶
      *        5. 返回  </XXX>
      */
     end: new AST.resolver({
       reg: /^<\/([A-z]+[0-9]?)>/,
       action: function (s) {
         // 1. 分组捕获 tagName
         let matchRes = s.match(this.reg);
         let tag = matchRes[1];
         // 2. 弹栈 mark
         let res = AST.mark.pop();
         // 比较两者是否相等
         if (tag != res)
           throw new Error("标签未闭合,期望值:" + tag + "->实际值:" + res);
         // 3. 弹栈 stack 弹出node对象
         let node = AST.stack.pop();
         // 4. 如果 stack 的栈顶没有 children 属性就添加一个空数组
         if (!AST.stack[AST.stack.length - 1].children)
           AST.stack[AST.stack.length - 1].children = [];
         // 将 node 作为children放入 stack的栈顶
         AST.stack[AST.stack.length - 1].children.push(node);
         return matchRes[0];
       },
     }),
 
     /**
      * @method  解析器四: 解析注释 <!-- -->
      * @desc 直接全部返回 <!-- -->
      */
     comments: new AST.resolver({
       reg: /^\s*<!\-\-[\s*\S*]*\-\->/,
       action: function (s) {
         // 1. 分组捕获 tagName
         let matchRes = s.match(this.reg);
         return matchRes[0];
       },
     }),
     /**
      * @method  解析器五: 解析标签间内的文本 > XXXX </ZZZ
      * @desc
      *        1. 将 XXX 提取出来作为 文本
      *        2. 如果文本不为空就创建一个文本节点，作为children放入 stack 栈顶， 其实是先压栈在弹栈，这里简化了
      *        3. 返回  `>` + XXXX  由于多返回了一个 `>` 所以这里  matchRes[1].length 需要加一
      */
     innertext: new AST.resolver({
       reg: /^>([^<>]*)<\//,
       action: function (s) {
         let matchRes = s.match(this.reg);
         // 去掉首位空格
         let text = matchRes[1].replaceAll(/^\s+|\s+$/g, "");
         // 文本非空判断
         if (!text) return s.substring(0, matchRes[1].length + 1);
         //  创建一个虚拟文本节点， 作为children放入 stack 栈顶
         if (!AST.stack[AST.stack.length - 1].children)
           AST.stack[AST.stack.length - 1].children = [];
         let textNode = new vDom("TEXT", 3, {});
         textNode.attrs["content"] = new vAttr(text);
         AST.stack[AST.stack.length - 1].children.push(textNode);
         // 返回
         return s.substring(0, matchRes[1].length + 1);
       },
     }),
 
     /**
      * @method  解析器六: 解析标签间外的文本 > XXXX <ZZZ  第一个 > 是没有的了
      * @desc
      *        1. 将 XXX 提取出来作为 文本
      *        2. 如果文本不为空就创建一个文本节点，作为children放入 stack 栈顶， 其实是先压栈在弹栈，这里简化了
      *        3. 返回  XXXX
      */
     outertext: new AST.resolver({
       reg: /^([^<>]*)</,
       action: function (s) {
         let matchRes = s.match(this.reg);
         // 去掉首位空格
         let text = matchRes[1].replaceAll(/^\s+|\s+$/g, "");
         // 文本非空判断
         if (!text) return s.substring(0, matchRes[1].length);
         //  创建一个文本节点， 作为children放入 stack 栈顶
         if (!AST.stack[AST.stack.length - 1].children)
           AST.stack[AST.stack.length - 1].children = [];
         let textNode = new vDom("TEXT", 3, {});
         textNode.attrs["content"] = new vAttr(text);
         AST.stack[AST.stack.length - 1].children.push(textNode);
         // 返回
         return s.substring(0, matchRes[1].length);
       },
     }),
   };
 
   /**
    * @namespace RENDERER
    * @desc
    *    渲染器，将虚拟节点渲染上树
    *
    * HISTORY:                                            *
    *     @Author: Malphite                                 *
    *     @Date: 2022-10-29
    *     @since: v4.0.0
    *
    * @desc
    *
    *    渲染
    *
    *  1. 首先是调用公共方法 {@linkplain RENDERER.render 渲染函数}
    *     判断传入的vDom的nodeType 如果是1就是普通节点{@linkplain RENDERER.renderDocumentNode 渲染普通节点}
    *     如果传入的nodeType是3就是文本节点{@linkplain RENDERER.renderTextNode 渲染文本节点}
    *
    *  2. 如果是渲染文本节点需要先创建一个新的空的文本节点dom,调用
    *     {@linkplain proxy.addListener 代理对象里面的方法添加事件监听}
    *     需要传入一个回调函数，此时会执行它，并且还会将它作为回调函数放入切面函数列表里面
    *     回调函数:
    *        1）、首先获取虚拟节点里面的属性值，文本节点的属性content代表它的文本值
    *        2）、判断这个虚拟节点里面有没有预处理参数，预处理参数在模版中以 XXX.YYY 出现， XXX可以被一个预处理参数取代
    *        3）、如果虚拟节点中有预处理参数，并且在属性的表达式中发现了预处理参数的表达就调用
    *    {@linkplain proxy.executeValue 代理对象里面的方法获取函数表达式的值} 执行并返回结果
    *            如果属性的表达式中发现了模版表达式 {{}} 就调用
    *    {@linkplain proxy.getTemplateValue 代理对象里面的方法获取模版表达式的值} 执行并返回结果
    *            如果上面的情况都没有命中就直接取表达式的值
    *    将最后得到的值放入虚拟属性里面的currentValue
    *        4）、将刚刚创建的文本节点的文本值设置为虚拟属性的currentValue
    *        5）、判断
    *            情况一：如果这个dom已经上树，说明这次是在切面回调里面触发的，上一步已经将最新值修改了，这里直接返回
    *            情况二: 如果这个虚拟节点的ele存在(ele是在上树完成后添加的，所以这个代表已经不是第一次执行了，是执行的切面回调)
    *                  并且dom已经下树，说明这次绑定已经失效，直接返回false让分发器移除这个回调
    *        6）、剩下的就是说明这个dom是新增的，需要返回上树。
    *
    *  3. 如果是渲染普通节点需要根据节点的tagName来创建一个HTMLElement(dom)
    *        首先处理虚拟节点里面的属性节点，因为属性里面可能内含指令，指令可能会修改后面的解析方式
    *        ***** 这里在解析属性时是按照从左到右的解析方式，所以需要早初始化的属性需要先声明 *****
    *
    *      --------- 渲染虚拟属性  start  -------------------
    *        遍历虚拟节点里面的所有虚拟属性。
    *            1）、{@linkplain RENDERER.getRenderers 获取所有的内置视图渲染器}  依次的遍历它们
    *            2）、一个{@linkplain RENDERER.renderer 渲染解析器 } 里面 包括两个部分
    *      reg -- 正则表达式，用于判断是否可以被该渲染解析器渲染  action -- 渲染方法*
    *            3）、判断这个虚拟属性的key是否可以被渲染解析器渲染，如果可以就调用
    *       {@linkplain proxy.addListener 代理对象里面的方法添加事件监听}
    *      需要传入一个回调函数，此时会执行它，并且还会将它作为回调函数放入切面函数列表里面
    *               回调函数:
    *                   3.1）、判断这个虚拟节点里面有没有预处理参数，预处理参数在模版中以 XXX.YYY 出现， XXX可以被一个预处理参数取代
    *                   3.2）、如果虚拟节点中有预处理参数，并且在属性的表达式中发现了预处理参数的表达就调用
    *             {@linkplain proxy.executeValue 代理对象里面的方法获取函数表达式的值} 执行并返回结果
    *                     如果属性的表达式中发现了模版表达式 {{}} 就调用
    *             {@linkplain proxy.getTemplateValue 代理对象里面的方法获取模版表达式的值} 执行并返回结果
    *                     如果上面的情况都没有命中就直接取 {@linkplain proxy.getValue 代理对象里面的方法获取vm的值} 执行并返回结果
    *             将最后得到的值放入虚拟属性里面的currentValue
    *                   3.3）、判断 如果这个虚拟节点的ele存在(ele是在上树完成后添加的，所以这个代表已经不是第一次执行了，是执行的切面回调)
    *             并且它已经下树，说明这个虚拟节点已经失效，直接返回false让分发器移除这个回调
    *                   3.4）、执行上面的渲染解析器的渲染方法，这个方法返回false也会使这个回调被分发器移除
    *          4）、如果上面被渲染会返回它的属性key，如果没有证明是普通的属性，直接使用属性表达式即可
    *          5）、如果这个虚拟节点被标记上了隐藏 visable === false 就跳过下面的子节点处理过程，返回空。这样也不会被上树
    *          6）、将dom放入虚拟节点的ele中
    *          7）、遍历虚拟节点的子节点，渲染子节点并将返回的子节点的dom加在当前的dom中
    *          8）、返回虚拟节点的ele -- 会被父节点接收或者去上树
    *
    *      visable 属性详见条件渲染
    *
    *      --------- 渲染虚拟属性  end  -------------------
    *
    *      插入上树规则 (参考 {@linkplain RENDERER.insertBefore 插入上树}) :
    *
    *      一般的，会将新增的节点插入到它兄节点的后面。
    *  1. 如果它没有兄节点，并且父节点此时没有子节点，那么由父节点 appendChild
    *  2. 如果它没有兄节点，并且父节点此时有子节点，那么就插入到父节点的第一个子节点之前
    *  3. 如果它有兄节点，并且兄节点visable !== false代表它已经上树，那么
    *        如果兄节点是父节点的最后一个节点，就由父节点 appendChild
    *        如果兄节点不是父节点的最后一个节点，那么就插入到兄节点的后一个节点的前面
    *  4. 如果它有兄节点，并且兄节点visable === false 那么递归的继续往上寻找兄节点，
    *        直到找到一个上树的兄节点执行 3 操作
    */
   var RENDERER = {
     /**
      * @method 渲染虚拟节点
      * @public
      * @desc
      *
      *    在渲染节点时需要带上父节点的信息，方便进行删除or插入等操作
      *    这个方法是提供出来在 proxy里面引用的，方法里面的this指向 VM对象
      * @param {*} vDom 待渲染节点
      * @param {*} pDom 父节点
      * @returns 渲染后的节点 domElement
      * @desc
      *
      *    如果返回了 domElement 可以用于上树 或者加入到父节点中
      *    如果返回null 说明无须上树(内部的文本节点己经上树，或者条件决定这个domElement不能上树 visable === false)
      */
     render(vDom, pDom) {
       return vDom.nodeType == 1
         ? RENDERER.renderDocumentNode.call(this, vDom, pDom)
         : RENDERER.renderTextNode.call(this, vDom, pDom);
     },
 
     /**
      * @method 渲染普通节点   this指向VM对象
      * @private
      * @param {*} vDom 待渲染节点
      * @param {*} pDom 父节点
      * @returns 渲染后的节点 domElement
      */
     renderDocumentNode(vDom, pDom) {
       let self = this;
       // 1. 通过tagName查找组件，直接返回组件渲染的结果即可 // TODO
       let dom = document.createElement(vDom.tagName);
       // 2. 渲染属性
       _.each(vDom.attrs, (attr, key) => {
         var res = "";
         Array.prototype.every.call(RENDERER.getRenderers(), function (r) {
           if (r.reg.test(key)) {
             self.proxy.addListener.call(self, function () {
               /**
                * 调用 {@linkplain proxy.parseAttr 获取虚拟属性当前值}
                */
               if (r.parse)
                 attr.currentValue = self.proxy.parseAttr.call(self, attr, vDom);
               // vDom已经完成初始化，但是 dom 已不在树上面，所以可以确定dom被移除，这个回调也应该移除
               if (vDom.ele && !document.contains(dom)) return false;
               // 到此，已经完成 expression -> attrValue 的过程了
               /**
                * 调用渲染解析器的渲染函数
                * @param {*} attr 虚拟属性对象 attr.currentValue就是解析出来的值
                * @param {*} dom  刚刚创建出来的element对象
                * @param {*} vDom 当前待解析的虚拟节点
                * @param {*} pDom 父虚拟节点
                */
               return r.action.call(r, key, attr, dom, vDom, pDom, self);
             });
             res = key;
             return false;
           }
           return true;
         });
         // 判断res 如果res 为空就按照普通的属性处理
         if (!res) {
           dom.setAttribute(key, attr.expression);
         }
       });
       //返回空节点
       if (vDom.visable === false) return null;
       /**
        * 将生成的dom挂在虚拟节点上面
        * 这里就添加，子节点解析时需要在父节点里面找到这个
        */
       vDom.ele = dom;
       //判断是否是组件
       if (self.proxy.components[vDom.tagName.toLowerCase()]) {
         // 先将这个节点上树，然后再加载组件
         // pDom.ele.appendChild(dom);
         // 加载组件
         RENDERER.parseComponentNode.call(self, vDom, pDom.ele);
         return;
       }
       // 3. 渲染子节点
       if (vDom.children.length > 0)
         _.each(vDom.children, (v, k) => {
           // 继承这个临时变量
           if (vDom.mark && !v.mark) {
             v.mark = vDom.mark;
             v.arg = vDom.arg;
           }
           // 声明子节点的key
           if (!v.key) v.key = k;
           // 递归的去渲染子节点
           let _child = RENDERER.render.call(self, v, vDom);
           // 如果有返回就appendChild
           if (_child) dom.appendChild(_child);
         });
       return vDom.ele;
     },
 
     /**
      * @method 渲染文本节点   this指向VM对象
      * @private
      * @param {*} vDom 待渲染节点
      * @param {*} pDom 父节点
      * @returns 渲染后的节点 domElement
      */
     renderTextNode(vDom, pDom) {
       let self = this;
       // 创建一个空的文本dom
       let dom = document.createTextNode("");
       self.proxy.addListener.call(this, function () {
         // 获取文本解析值，这个一定是要带花括号的，没有花括号的不做解析
         // 获取文本 虚拟属性节点 attrs里面的content项
         let attr = vDom.attrs.content;
         let flag = !!vDom.mark;
         if (
           flag &&
           new RegExp("{{" + vDom.mark + "[\\s\\S]*}}").test(attr.expression)
         ) {
           // 如果是含临时变量，而且表达式里面有临时变量的表达式，就构建一个函数去获取值
           attr.currentValue = self.proxy.executeValue.call(
             self,
             attr.expression,
             vDom.mark,
             vDom.arg
           );
         } else if (/{{[\s\S]*}}/.test(attr.expression)) {
           //带模版解析
           attr.currentValue = self.proxy.getTemplateValue.call(
             self,
             attr.expression
           );
         } else {
           attr.currentValue = attr.expression;
         }
         // 将上面文本dom的文本值修改成当前的最新值
         dom.textContent = attr.currentValue;
         // 如果这个文本dom还在树上，那么这个修改的操作就已经结束了
         if (document.contains(dom)) return;
         // vDom已经完成初始化，但是 dom 已不在树上面，所以可以确定dom被移除，这个回调也应该移除
         if (vDom.ele && !document.contains(dom)) return false;
         // 将当前的dom放入虚拟节点中
         vDom.ele = dom;
       });
       return vDom.ele;
     },
 
     /**
      * @method 渲染组件   this指向VM对象
      * @param {*} vDom 待渲染节点
      */
     parseComponentNode(vDom, parent) {
       let self = this;
       let componentStr = vDom.tagName.toLowerCase();
       let component = self.proxy.components[componentStr];
       let param = {};
       if (_.isArray(component)) {
         param = _.isFunction(component[1])
           ? component[1].apply(self)
           : component[1];
         component = component[0];
       }
       let key = self[componentStr]
         ? RENDERER.getVDomAttr(vDom, "key")
         : componentStr;
       let vc = component.create(param);
       vc.$mount(parent);
       if (!self.$children) self.$children = [];
       self.$children.push(vc);
       vc.super = self;
       if (!self[key]) self[key] = vc;
     },
 
     /**
      * @method 插入上树
      * @desc
      *    将vDom 解析并插入到 pDom的合适的位置 -- 放到指定节点之后
      *    this指向VM对象
      * @param {*} vDom 待渲染节点
      * @param {*} pDom 父节点
      * @param {*} pointer 当前指针
      * @param {*} array 父节点的children
      */
     insertNode: function (vDom, pEle, pDom, pointer, array) {
       /**
        * 如果根据传入的指针没有找到对应的兄节点  !array[pointer]
        * 或者根据传入的指针找到的兄节点没有上树  array[pointer] && array[pointer].visable === false
        *
        * 上面两个条件有一个触发，并且指针大于0,还没有指完。那么指针减一，递归继续查找
        *
        */
       const flag =
         !array[pointer] || (array[pointer] && array[pointer].visable === false);
       if (flag && pointer > 0) {
         return RENDERER.insertNode.call(
           this,
           vDom,
           pEle,
           pDom,
           --pointer,
           array
         );
       }
       /**
        * 到这里说明已经是可以将虚拟节点上树了。调用方法 {@linkplain RENDERER.render 渲染虚拟节点}
        */
       let _child = RENDERER.render.call(this, vDom, pDom);
       // 如果没有生成，说明可能是这个节点不显示
       if (!_child) return;
 
       // 1.如果还是没有找到兄节点，或者兄节点没有上树
       if (!array[pointer] || array[pointer].visable === false) {
         /**
          * 1.1 如果父节点上面有子节点上树，那么插入到它第一个子节点之前
          * 1.2 如果父节点上面没有子节点上树，那么直接插入到父节点上面
          */
         if (pEle.childNodes.length > 0) {
           pEle.insertBefore(_child, pEle.childNodes[0]);
         } else {
           pEle.appendChild(_child);
         }
       } else {
         // 找到符合添加的dom，就加到它的后面
         // 判断这个dom是不是最后一个
         /**
          * 如果找到的兄节点是父节点的最后一个节点，那么直接插入到父节点上面
          * 如果找到的兄节点不是父节点的最后一个节点，那么插入到兄节点的下一个节点的前面
          */
         if (pEle.lastChild == array[pointer].ele) {
           // 在pDom紧挨着之后添加
           pEle.appendChild(_child);
         } else {
           pEle.insertBefore(_child, array[pointer].ele.nextSibling);
         }
       }
       // 将dom放入虚拟节点中
       vDom.ele = _child;
     },
 
     /**
      * 渲染解析器
      * @constructs
      * @lends RENDERER.renderer
      * @param {*} reg 正则表达式
      * @param {*} action 渲染方法
      *    参数是
      *        @param {*} attr 虚拟属性对象 attr.currentValue就是解析出来的值
      *        @param {*} dom  刚刚创建出来的element对象
      *        @param {*} vDom 当前待解析的虚拟节点
      *        @param {*} pDom 父虚拟节点
      *        @param {*} self 当前的vm对象
      * @param {*} parse 是否需要解析表达式
      */
     renderer: function (reg, action, parse = true) {
       if (Object.prototype.toString.call(reg) !== "[object RegExp]") {
         action = reg.action;
         reg = reg.reg;
       }
       this.reg = reg;
       this.parse = parse;
       this.action = (...param) => action.apply(this, param);
     },
 
     /**
      * v- 指令
      * 指令解析器
      * @constructs
      * @lends RENDERER.directive
      * @param {*} bind   用于在指令初次执行时调用
      * @param {*} update 用于在指令更新的时候被调用
      *    回调参数
      *    @param {*} ele  当前指令对应的dom对象
      *    @param {*} binding  参数详见 {@linkplain MyRenders.directive 指令渲染} 里面的参数定义
      *      def： 匹配上的指令对象
      *      expression: 该指令对应的表达式
      *      name：指令名称 去掉 v- 的部分
      *      rawName: 指令原称
      *      value: 属性当前的解析值
      *      modifiers: 额外参数
      *          vm  vm对象
      *          attr  虚拟属性对象
      *          vDom  虚拟节点
      *          pDom  父虚拟节点
      */
     directive: function (bind, update) {
       this.bind = bind;
       this.update = update || bind;
     },
 
     /**
      * 获取虚拟dom里面对应属性的值
      * @param {*} vDom  虚拟dom
      * @param {*} key   属性的key
      * @returns
      */
     getVDomAttr(vDom, key) {
       if (!vDom.attrs[key]) return "";
       return vDom.attrs[key].expression || "";
     },
 
     /**
      * 获取名称为key的指令解析器
      * @param {*} key  指令名称
      * @returns  指令解析器 or null
      */
     getDirectives(key) {
       if (!RENDERER.directives) {
         RENDERER.directives = {};
         RENDERER.directives["each"] = MyDirectives.each;
         RENDERER.directives["if"] = MyDirectives.if;
         RENDERER.directives["else"] = MyDirectives.else;
         RENDERER.directives["clock"] = MyDirectives.clock;
         RENDERER.directives["model"] = MyDirectives.model;
       }
       return this.proxy.diretives[key] || RENDERER.directives[key];
     },
 
     /**
      * 获取全部的渲染解析器
      * @returns
      */
     getRenderers() {
       if (!RENDERER.renderers) RENDERER.initRenderer();
       return RENDERER.renderers;
     },
 
     /**
      * 初始化渲染解析器
      */
     initRenderer() {
       if (!RENDERER.renderers) RENDERER.renderers = [];
       RENDERER.renderers.push(MyRenders.directive);
       RENDERER.renderers.push(MyRenders.dynamic);
       RENDERER.renderers.push(MyRenders.event);
     },
   };
 
   /**
    * @namespace MyRenders
    * @desc
    *    保存内置的几种属性渲染解析器
    *
    *    1. {@linkplain MyRenders.directive 指令属性渲染解析器} 它匹配的是 v-XXX的属性
    *    2. {@linkplain MyRenders.dynamic 动态属性渲染解析器} 它匹配的是 :XXX的属性
    *    3. {@linkplain MyRenders.event 事件属性渲染解析器} 它匹配的是 @XXX的属性
    *
    *
    *
    */
   var MyRenders = {
     /**
      * 情况一: 匹配 v-XXX 的属性值，这个是作为指令；来进行解析的
      * 解析成功返回 属性匹配值,这个属性将不会出现在真实dom上,
      * 解析失败返回空字符串，让解析器按照默认的方式继续解析这个属性
      */
     directive: new RENDERER.renderer({
       reg: /^v-(\w*\-?\w*)$/,
       action: function (key, attr, dom, vDom, pDom, VM) {
         // 获取当前的指令名称
         let matchRes = key.match(this.reg);
         let directive = RENDERER.getDirectives.call(VM, matchRes[1]);
         if (!directive) return;
         let binding = {
           def: directive,
           expression: attr.expression,
           modifiers: {
             vm: VM,
             attr: attr,
             vDom: vDom,
             pDom: pDom,
           },
           name: matchRes[1],
           rawName: key,
           value: attr.currentValue,
         };
         return vDom.ele
           ? directive.update(dom, binding)
           : directive.bind(dom, binding);
       },
     }),
 
     /**
      * 情况二: 匹配 :XXX 的属性值，这个是作为动态属性进行解析的
      * class  style 和原始的进行合并
      * input 和 textarea 的value值有特殊赋值方式
      */
     dynamic: new RENDERER.renderer({
       reg: /^:(\w*\-?\w*)$/,
       action: function (key, attr, dom, vDom, pDom, VM) {
         // 获取当前的属性值
         let matchRes = key.match(this.reg);
         // 特殊处理  class  style 的 合并  input 和 textarea 的 赋值
         if ("class" == matchRes[1]) {
           // class属性的特殊处理  这里的情况是 class = ""  :class = ""
           // 首先获取旧的class取值
           if (!attr.oldClass) attr.oldClass = dom.classList.value;
           // 然后重置class值
           dom.setAttribute("class", attr.oldClass || "");
           // 然后按照属性添加
           let value = _.isArray(attr.currentValue)
             ? attr.currentValue
             : attr.currentValue.split(",");
           _.each(value, (v) => dom.classList.add(v));
           return;
         }
         if ("style" == matchRes[1]) {
           // style属性的特殊处理  这里的情况是 style = ""  :style = ""
           // 首先获取旧的style取值
           if (!attr.oldStyle) attr.oldStyle = dom.style.cssText;
           // 然后重置style值
           dom.setAttribute("style", attr.oldStyle || "");
           // 然后按照属性添加
           _.each(attr.currentValue, (v, k) => dom.style.setProperty(k, v));
           return;
         }
         if ("value" == matchRes[1]) {
           if ("input" == vDom.tagName || "textarea" == vDom.tagName) {
             dom.value = attr.currentValue;
             return;
           }
         }
         dom.setAttribute(matchRes[1], attr.currentValue);
       },
     }),
 
     /**
      * 情况三: 匹配 @XXX 的属性值，这个是作为动态绑定事件
      * 这里将parse设置为false防止多次绑定和解析属性时就去执行一次
      */
     event: new RENDERER.renderer(
       {
         reg: /^@(\w*\-?\w*)$/,
         action: function (key, attr, dom, vDom, pDom, VM) {
           // 首先判断是否第一次进入
           if (vDom.ele) return;
           // 获取当前的属性值,事件名称
           let matchRes = key.match(this.reg);
           // 对表达式进行拆分，获取方法体的参数  XXX (YYY)
           let matchExp = attr.expression.match(
             /^([\s\S+]+)(\([^\(\)]*\))?\s*$/
           );
           // 对方法体获取值
           let fn = VM.proxy.getValue.call(VM, matchExp[1].trim());
           // 获取参数
           let mark = vDom.mark || "b";
           let arg = vDom.arg || "b";
           // 添加事件
           dom.addEventListener(matchRes[1], function (e) {
             if (fn && _.isFunction(fn)) {
               if (/\([^\(\)]*\)/.test(attr.expression)) {
                 //说明可以找到这个function 就直接 b.
                 new Function(
                   mark,
                   constant.mark,
                   "e",
                   constant.mark + constant.separator + attr.expression.trim()
                 ).call(VM, arg, VM, e);
               } else {
                 // 说明直接执行这个方法即可
                 VM.proxy.getValue.call(VM, matchExp[1].trim()).call(VM, e);
               }
             } else {
               try {
                 // 首先尝试直接调用
                 new Function(
                   mark,
                   constant.mark,
                   "e",
                   attr.expression.trim()
                 ).call(VM, arg, VM, e);
               } catch (e) {
                 // 然后再尝试添加 b.
                 new Function(
                   mark,
                   constant.mark,
                   "e",
                   constant.mark + constant.separator + attr.expression.trim()
                 ).call(VM, arg, VM, e);
               }
             }
           });
         },
       },
       null,
       false
     ),
   };
 
   /**
    * @namespace MyDirectives
    * @desc
    *    保存内置的几种指令解析器
    *
    *    1. {@linkplain MyDirectives.each 内置指令 v-each}  循环渲染
    *    2. {@linkplain MyDirectives.if 内置指令 v-if}  条件渲染
    *    3. {@linkplain MyDirectives.else 内置指令 v-else}  条件渲染
    *    4. {@linkplain MyDirectives.clock 内置指令 v-clock}  这个是直接移除这个属性
    *    5. {@linkplain MyDirectives.model 内置指令 v-model}  表单元素双向绑定
    */
   var MyDirectives = {
     /**
      * 内置指令 v-each
      *    属性值指向的是一个可以访问到的list，根据这个list来循环的渲染
      *    1. 首先进行的是数据初始化: 这个指令除了属性值，还提供了下面的属性来描述这次的渲染。
      *        (1)、key 指的是这次渲染的主键 -- 对应list对象里面的key。
      *    由于内部排序时一度将list信息转移成object，所以这里是不允许主键重复的，否则会渲染出错
      *    建议加上主键，由于没有diff算法，都是通过主键来标识dom的，为了减小更新时的改动，建议加上主键
      *        (2)、mark 指的是在循环体中使用一个字符串来代表这一循环list里面的信息。
      *    一般建议要加上，而且在同一个嵌套循环中不能重复使用，避免的内外循环的冲突
      *        (3)、order asc 升序,默认 desc 降序
      *    一般的，渲染的顺序是以主键key来进行排序，支持数字，时间和字符。
      *        (4)、lay-event 回调函数，直接挂载在vm下面的一个函数的名称。this指向和参数和本方法一致
      *    2. 获取解析子节点模版
      *        直接获取这个vDom下面的children下面的第一项。
      *        由于这里循环渲染了，一般不建议下面再添加其它子节点，所以该节点下面的子节点只能有一个
      *    3. 创建缓存节点列表
      *        这个缓存节点列表保存在这个vDom下面，作为每次更新时候的参考，在节点更新的最后会将它设置为最新的。
      *    4. 创建比较映射 -- 缓存目录
      *        根据3中的信息，创建一个object，方便下面的比较
      *    5. 创建循环list
      *        没有使用原始的数据进行循环遍历
      *        数组需要排序，排序又会引起数组改变，数组改变又会来更新这个节点，造成一个死循环。所以这里拷贝一个数组来进行排序的操作。
      *    6. 创建临时数组，保存遍历信息。
      *    ---           准备工作结束       -------
      *
      *    7. 遍历循环list ,进行比较更新
      *
      *      这里的插入都是往后插入: 由于是顺序遍历的，后一个遍历结果需要插入到前一个遍历结果后面，所以这里的插入策略都是往后插入
      *
      *        (1)、 如果遍历的一项没有出现在缓存目录上，说明它是新来的，直接创建并插入，将它的信息放入临时数组中。
      *        (2)、 如果遍历的一项,它的mark对应的值和原来的不同，就粗略的觉得它改变了，删除原来的节点重新插入。移除缓存目录里面的对应项
      *        (3)、 如果遍历的一项,它的mark对应的值和原来的一样，简单的觉得不用发生改变。移除缓存目录里面的对应项
      *
      *      由于没有精细的diff算法来判断节点内部的变化，所以某一项只要有一点改变就都是删除了重新插入的。
      *
      *    8. 删除多余节点
      *       这个时候还存在与缓存目录里面的项就是多余的，直接删除
      *    9. 更新缓存节点列表
      *    10. 执行回调
      *
      * 由于优化了mark的使用，对于嵌套循环有了一些优化
      *
      *  before :  对于内部嵌套的值需要多加 {{}} ,每深一层就要多加一层
      *  now:      多层嵌套里面的取值得到了改变
      *
      *            1. 属性值不用使用 {{}} 修改 直接 mark.XXX
      *            2. 文本值使用一层 {{}} 即可
      */
     each: new RENDERER.directive(function (ele, binding) {
       /**
        * 1. 初始化数组, 获取属性值  keyStr 主键在循环内容里面的key、 markStr  mark标志在循环内容里面的key
        *                       orderStr 排序规则 、 callBack 回调函数 在vm里面的key
        */
       let vDom = binding.modifiers.vDom;
       let attr = binding.modifiers.attr;
       let keyStr = RENDERER.getVDomAttr(vDom, "key");
       let markStr = RENDERER.getVDomAttr(vDom, "mark");
       let orderStr = RENDERER.getVDomAttr(vDom, "order");
       // 特殊处理 order属性只支持 desc 和 asc 默认是 asc
       orderStr = orderStr === "desc" ? "desc" : "asc";
       let callBack = RENDERER.getVDomAttr(vDom, "lay-event");
 
       /**
        * 2. 获取子节点模版
        *   如果没有模版就从虚拟dom的children里面取第一个,如果没有就空着
        */
       if (!vDom.eachTemplate) {
         vDom.eachTemplate = "";
         if (vDom.children && vDom.children.length > 0)
           vDom.eachTemplate = vDom.children.pop();
       }
 
       /**
        * 3. 创建缓存节点列表
        */
       if (!vDom.eachChildren) vDom.eachChildren = [];
       /**
        * 4. 创建比较映射 -- 缓存目录
        */
       vDom.tempChildren = {};
       _.each(
         vDom.eachChildren,
         (ec, ek) =>
           (vDom.tempChildren[ec.key] = {
             index: ek,
             value: ec,
           })
       );
 
       /**
        * 5. 创建循环list
        */
       let list = [];
       _.each(attr.currentValue, (v, k) => {
         let key = keyStr ? v[keyStr] : k;
         list.push({
           key: key,
           index: k,
           value: v,
         });
       });
       // 排序
       list = keyStr
         ? _.sortBy(list, (o) => {
             if (/^\d+$/.test(o.value[keyStr])) {
               return orderStr !== "desc"
                 ? -Number(o.value[keyStr])
                 : Number(o.value[keyStr]);
             } else if (/^\d{2,4}\-\d{2}\-\d+/.test(o.value[keyStr])) {
               return orderStr !== "desc"
                 ? -new Date(o.value[keyStr].replace(/-/g, "/"))
                 : new Date(o.value[keyStr].replace(/-/g, "/"));
             } else {
               return orderStr !== "desc"
                 ? -o.value[keyStr].charCodeAt()
                 : o.value[keyStr].charCodeAt();
             }
           })
         : list;
       let currentChildren = [];
       _.each(list, (v, k) => {
         let key = v.key;
         let childrenNode = _.cloneDeep(vDom.eachTemplate);
         childrenNode.key = key;
         childrenNode.mark = markStr;
         childrenNode.arg = v.value;
         // 比较并进行diff
         // case 01 这个节点之前完全没有加入
         if (!vDom.tempChildren[key]) {
           // 获取前一个的下标
           RENDERER.insertNode.call(
             binding.modifiers.vm,
             childrenNode,
             ele,
             vDom,
             k - 1,
             currentChildren
           );
           // 最后加入
           currentChildren.push(childrenNode);
         } else if (!_.isEqual(v.value, vDom.tempChildren[key].value.arg)) {
           // case 02 这个节点和之前的不一样了
           // 插入新的，移除旧的
           // 获取前一个的下标
           RENDERER.insertNode.call(
             binding.modifiers.vm,
             childrenNode,
             ele,
             vDom,
             k - 1,
             currentChildren
           );
           // 移除旧的
           vDom.ele.removeChild(vDom.tempChildren[key].value.ele);
           delete vDom.tempChildren[key];
           // 最后加入
           currentChildren.push(childrenNode);
         } else {
           // case 03 保持原样
           currentChildren.push(vDom.tempChildren[key].value);
           delete vDom.tempChildren[key];
         }
       });
       // 删除多余节点
       _.each(vDom.tempChildren, (v) => vDom.ele.removeChild(v.value.ele));
       vDom.eachChildren = currentChildren;
       // 触发回调
       callBack &&
         binding.modifiers.vm[callBack].call(binding.modifiers.vm, ele, binding);
     }),
 
     /**
      * 内置指令 v-if
      * 属性值指向的是一个boolean类型的值，当它为true的时候这个指令控制的dom显示，当它为false的时候隐藏
      * 1. visable 标志是 false dom 隐藏  已经上树的移除
      * 2. visable 标志是 true dom 重新解析并插入到兄节点之后
      */
     if: new RENDERER.directive(function (ele, binding) {
       let vDom = binding.modifiers.vDom;
       let pDom = binding.modifiers.pDom;
       let attr = binding.modifiers.attr;
       // 判断是否是第一次
       let first = !attr.first;
       if (first) attr.first = true;
       // 添加判断，当属性没有变化时阻止继续执行，防止循环渲染
       if (attr.value === attr.currentValue) return;
       // 赋值
       attr.value = attr.currentValue;
       // 首先设置dom的visable 标志 true是显示  false 是隐藏
       vDom.visable = !!attr.currentValue;
       if (!vDom.visable) {
         // visable 标志是 false dom 隐藏
         // 删除原有的dom
         if (vDom.ele && document.contains(vDom.ele)) {
           // 如果虚拟dom已经解析，并且已经上树，就从父节点上面移除
           pDom.ele.removeChild(vDom.ele);
         }
         // 不管有没有解析，有没有上树, 重置虚拟节点下面的ele
         vDom.ele = null;
         return;
       }
       // 处于父节点解析时上面的 visable 设置好了，解析父节点时会自动将这个节点插入  这个主要是 each 时的特殊处理
       if (!pDom.ele) return;
       // 第一次就不插入了，正常解析即可
       if (first) return;
       // 删除原有的
       // visable 标志是 true dom 重新解析并插入到兄节点之后
       let array =
         pDom.children && pDom.children.length > 0
           ? pDom.children
           : pDom.eachChildren;
       let pointer = array.indexOf(vDom) - 1;
       RENDERER.insertNode.call(
         binding.modifiers.vm,
         vDom,
         pDom.ele,
         pDom,
         pointer,
         array
       );
     }),
 
     /**
      * 内置指令 v-else
      * 属性值指向的是一个boolean类型的值，当它为true的时候这个指令控制的dom显示，当它为false的时候隐藏
      * 1. visable 标志是 false dom 隐藏  已经上树的移除
      * 2. visable 标志是 true dom 重新解析并插入到兄节点之后
      */
     else: new RENDERER.directive(function (ele, binding) {
       let vDom = binding.modifiers.vDom;
       let pDom = binding.modifiers.pDom;
       let attr = binding.modifiers.attr;
       // 判断是否是第一次
       let first = !attr.first;
       if (first) attr.first = true;
       // 首先查找它的兄节点
       let array =
         pDom.children && pDom.children.length > 0
           ? pDom.children
           : pDom.eachChildren;
       // 说明最多才一个节点，没得兄节点
       if (array.length < 2) return;
       // 声明一个虚拟属性对象，查找兄节点里面的vif虚拟属性节点
       let bAttr = null;
       _.each(array, (v, k) => {
         if (_.isEqual(v, vDom)) {
           // k - 1 获取兄节点，查看它的if表达式的值
           let _dom = array[k - 1];
           if (_dom.attrs["v-if"]) bAttr = _dom.attrs["v-if"];
         }
       });
       // 如果兄节点没有v-if属性就不解析了
       if (!bAttr) return;
       let flag = false;
       // 如果这个没有表达式，就取它兄节点的虚拟属性节点来进行判断
       // 如果有表达式就使用当前虚拟属性节点的当前值做判断
       if (!binding.expression) {
         let vm = binding.modifiers.vm;
         let res = vm.proxy.parseAttr.call(vm, bAttr, vDom);
         flag = !res;
       } else {
         flag = binding.value;
       }
       // 添加判断，当属性没有变化时阻止继续执行，防止循环渲染
       if (attr.value === flag) return;
       // 赋值
       attr.value = flag;
       // 然后和上面的if处理方式相同
       // 首先设置dom的visable 标志 true是显示  false 是隐藏
       vDom.visable = flag;
       if (!vDom.visable) {
         // visable 标志是 false dom 隐藏
         // 删除原有的dom
         if (vDom.ele && document.contains(vDom.ele)) {
           // 如果虚拟dom已经解析，并且已经上树，就从父节点上面移除
           pDom.ele.removeChild(vDom.ele);
         }
         // 不管有没有解析，有没有上树, 重置虚拟节点下面的ele
         vDom.ele = null;
         return;
       }
       // 处于父节点解析时上面的 visable 设置好了，解析父节点时会自动将这个节点插入
       if (!pDom.ele) return;
       // 第一次就不插入了，正常解析即可
       if (first) return;
       // visable 标志是 true dom 重新解析并插入到兄节点之后
       let pointer = array.indexOf(vDom) - 1;
       RENDERER.insertNode.call(
         binding.modifiers.vm,
         vDom,
         pDom.ele,
         pDom,
         pointer,
         array
       );
     }),
     /**
      * 内置指令 v-clock
      * 配合css 提前隐藏，在解析的时候将它拿掉
      */
     clock: new RENDERER.directive(function (ele, binding) {
       // 啥也不干
     }),
 
     /**
      * 内置指令 v-model
      * 实现  表单元素  和数据之间的双向绑定
      */
     model: new RENDERER.directive(function (ele, binding) {
       let vDom = binding.modifiers.vDom;
 
       // 首先要判断它是哪一种表单元素然后根据条件作出响应的处理
       let type = RENDERER.getVDomAttr(vDom, "type");
       if ("checkbox" == type) {
         // 多选框的渲染
         FormRender.renderCheckBox(ele, binding);
         return;
       }
 
       if ("radio" == type) {
         // 单选框的渲染
         FormRender.renderRadio(ele, binding);
         return;
       }
 
       if ("select" == vDom.tagName.toLowerCase()) {
         // 下拉框的渲染
         FormRender.renderSelect(ele, binding);
         return;
       }
 
       // 其它的是普通的input框
       FormRender.renderInput(ele, binding);
     }),
   };
 
   /**
    * @namespace FormRender
    *
    * @desc
    *
    *    表单渲染相关
    *
    */
   var FormRender = {
     // 复选框: 复选框对应的是string类型的数组
     renderCheckBox(ele, binding) {
       // 获取它的value 和checked
       let value = ele.value;
       // let checked = ele.checked;
       // 判断获取的值里面有没得这一项
       let data = binding.value;
       // 这里就切换了checkbox
       ele.checked = _.indexOf(data, value) >= 0;
       // 判断是否需要检查当前的这个表单元素
       if (false !== ele.getAttribute(constant.LAYUI_LAZY))
         FormRender.verifyItem(ele);
       let vDom = binding.modifiers.vDom;
       // ele的落地是在属性解析之后，所以可以通过这个判断是否第一次解析
       if (vDom.ele) return;
       let VM = binding.modifiers.vm;
       let attr = binding.modifiers.attr;
       // 接下来需要在切换checkbox时修改源数值
       ele.addEventListener("change", function () {
         // 这里需要重新获取值，否则不能达到修改源数据的效果
         let _data = VM.proxy.parseAttr.call(VM, attr, vDom);
         if (this.checked) {
           if (_.indexOf(_data, value) < 0) _data.push(value);
         } else {
           if (_.indexOf(_data, value) >= 0)
             _data.splice(_.indexOf(_data, value), 1);
         }
       });
     },
 
     // 单选框: 单选框对应的是一个字符串
     renderRadio(ele, binding) {
       // 获取它的value 和checked
       let value = ele.value;
       let checked = ele.checked;
       // 判断获取的值里面有没得这一项
       let data = binding.value;
       // 这里就切换了checkbox
       ele.checked = data == value;
       // 判断是否需要检查当前的这个表单元素
       if (false !== ele.getAttribute(constant.LAYUI_LAZY))
         FormRender.verifyItem(ele);
       let VM = binding.modifiers.vm;
       let vDom = binding.modifiers.vDom;
       // ele的落地是在属性解析之后，所以可以通过这个判断是否第一次解析
       if (vDom.ele) return;
       // 如果当前的值为空，但是这个单选框选中，就将这个单选框的值赋给绑定的变量值
       if (!data && checked) {
         if (vDom.mark) {
           VM.proxy.getMarkValue.call(
             VM,
             binding.expression,
             value,
             vDom.mark,
             vDom.arg
           );
         } else {
           VM.proxy.getValue.call(VM, binding.expression, value);
         }
         data = value;
         ele.checked = data == value;
       }
       // 接下来需要在切换radio时修改源数值
       ele.addEventListener("change", function () {
         if (this.checked) {
           if (vDom.mark) {
             VM.proxy.getMarkValue.call(
               VM,
               binding.expression,
               value,
               vDom.mark,
               vDom.arg
             );
           } else {
             VM.proxy.getValue.call(VM, binding.expression, value);
           }
         }
       });
     },
 
     // 下拉框: 下拉框对应的是一个字符串
     renderSelect(ele, binding) {
       // 赋值
       ele.value = binding.value;
       // 判断是否需要检查当前的这个表单元素
       if (false !== ele.getAttribute(constant.LAYUI_LAZY))
         FormRender.verifyItem(ele);
       let vDom = binding.modifiers.vDom;
       // ele的落地是在属性解析之后，所以可以通过这个判断是否第一次解析
       if (vDom.ele) return;
       let VM = binding.modifiers.vm;
       // 第一次的时候要将它下面的option设置对，循环渲染的不管
       // 这里使用直接删除虚拟属性的方式是因为，到现在，子节点option还没有开始解析
       let flag = false;
       _.each(vDom.children, (v) => {
         if (v.tagName.toLowerCase() == "option") {
           let _value = RENDERER.getVDomAttr(v, "value");
           if (binding.value == _value) {
             // 选中,给这个添加一个 selected 的属性
             if (!v.attrs["selected"]) v.attrs["selected"] = new vAttr(true);
             flag = true;
           } else {
             if (v.attrs["selected"]) delete v.attrs["selected"];
           }
         }
       });
       // 如果当前值没有匹配上任何一个选项值，就取第一个来赋值
       if (!flag && vDom.children.length > 0) {
         let firstValue = RENDERER.getVDomAttr(vDom.children[0], "value");
         if (vDom.mark) {
           VM.proxy.getMarkValue.call(
             VM,
             binding.expression,
             firstValue,
             vDom.mark,
             vDom.arg
           );
         } else {
           VM.proxy.getValue.call(VM, binding.expression, firstValue);
         }
         // 重新对下拉框进行赋值
         ele.value = firstValue;
       }
       // 接下来需要在切换radio时修改源数值
       ele.addEventListener("change", function () {
         if (vDom.mark) {
           VM.proxy.getMarkValue.call(
             VM,
             binding.expression,
             this.value,
             vDom.mark,
             vDom.arg
           );
         } else {
           VM.proxy.getValue.call(VM, binding.expression, this.value);
         }
       });
     },
 
     // 其它: 对应的是一个字符串 input框 等、
     renderInput(ele, binding) {
       // 赋值
       ele.value = binding.value;
       // 判断是否需要检查当前的这个表单元素
       if (false !== ele.getAttribute(constant.LAYUI_LAZY))
         FormRender.verifyItem(ele);
       // 判断是否需要进行表单校验  lay-lazy 设置为false不需要校验，直接跳过
       let vDom = binding.modifiers.vDom;
       // ele的落地是在属性解析之后，所以可以通过这个判断是否第一次解析
       if (vDom.ele) return;
       let VM = binding.modifiers.vm;
       // 检查是否有lay-lazy属性来判断添加什么样的监听事件
       let eventKey = vDom.attrs[constant.LAYUI_LAZY]
         ? "blur"
         : "input onpropertychange";
       // 接下来需要在切换radio时修改源数值
       _.each(eventKey.split(" "), (v) => {
         ele.addEventListener(v, function () {
           if (vDom.mark) {
             VM.proxy.getMarkValue.call(
               VM,
               binding.expression,
               this.value,
               vDom.mark,
               vDom.arg
             );
           } else {
             VM.proxy.getValue.call(VM, binding.expression, this.value);
           }
         });
       });
     },
 
     // 检查判断一个表单元素当前的值是否正常 from  layui.form
     verifyItem(item) {
       // 表单校验基于layui的form模块，没有就返回true 跳过校验
       if (!layui || !layui.form) return true;
       // layui的form模块是需要使用jQuery的，所以这里暂时可以使用$
       let othis = $(item);
       let parent = othis.parents().filter(".layui-form");
       let filter = parent.attr("lay-filter");
       let verifys = [];
       const defaultText = "请完成表单!";
       // 获取校验规则
       if (othis.attr(constant.LAYUI_VERIFY))
         verifys = othis.attr(constant.LAYUI_VERIFY).split("|");
       // 单选框和多选框的非空校验
       const checkflag =
         ("radio" === item.type || "checkbox" === item.type) &&
         verifys.length > 0 &&
         verifys[0] == "required";
       if (checkflag) {
         let index = 0;
         parent.find('[name="' + item.name + '"]').each(function () {
           if ($(this).get(0).checked === true) index++;
         });
         if ("checkbox" === item.type && index > 0) return true;
         if ("radio" === item.type && index === 1) return true;
         layui.layer.msg(othis.attr(constant.LAYUI_VERIFY_TEXT) || defaultText, {
           icon: 5,
         });
         return false;
       }
       // 剩下的就只有下拉框和输入框了
       let verifyType = othis.attr(constant.LAYUI_VERIFY_TYPE);
       let value = othis.val();
       let verify = layui.form.config.verify;
       let flag = true;
       _.every(verifys, function (v) {
         let isTrue = true,
           errorText = "",
           isFunction = _.isFunction(verify[v]);
         if (verify[v]) {
           isTrue = isFunction
             ? (errorText = verify[v](value, item))
             : !verify[v][0].test(value);
           errorText = errorText || verify[v][1];
           if (v === "required")
             errorText = othis.attr(constant.LAYUI_VERIFY_TEXT) || errorText;
           if (isTrue) {
             let parentNode =
               othis.parents().filter(".layui-form-scrollNode").length == 1
                 ? othis.parents().filter(".layui-form-scrollNode")
                 : othis.parents().filter(".layui-layer-content");
             let scrollFlag =
               parentNode.length == 1 &&
               (parentNode.get(0).scrollHeight >
                 parentNode.get(0).clientHeight ||
                 parentNode.get(0).offsetHeight >
                   parentNode.get(0).clientHeight);
             let proxyOthis =
               othis.get(0).tagName.toLowerCase() === "select" ||
               /^checkbox|radio$/.test(othis.get(0).type)
                 ? othis.next()
                 : othis;
             if (verifyType === "tips") {
               !scrollFlag &&
                 proxyOthis.get(0).getBoundingClientRect().height > 0 &&
                 layui.layer.tips(
                   errorText,
                   (function () {
                     if (typeof othis.attr("lay-ignore") !== "string") {
                       if (
                         othis.get(0).tagName.toLowerCase() === "select" ||
                         /^checkbox|radio$/.test(othis.get(0).type)
                       ) {
                         return othis.next();
                       }
                     }
                     return othis;
                   })(),
                   { tips: 1, tipsMore: true }
                 );
             } else if (verifyType === "alert") {
               layui.layer.alert(errorText, { title: "提示", shadeClose: true });
             } else {
               layui.layer.msg(errorText, { icon: 5, shift: 6 });
             }
             scrollFlag &&
               parentNode.animate(
                 {
                   scrollTop:
                     othis.offset().top -
                     parentNode.offset().top +
                     parentNode.scrollTop(),
                 },
                 800,
                 function () {
                   verifyType === "tips" &&
                     proxyOthis.get(0).getBoundingClientRect().height > 0 &&
                     layui.layer.tips(
                       errorText,
                       (function () {
                         if (typeof othis.attr("lay-ignore") !== "string") {
                           if (
                             othis.get(0).tagName.toLowerCase() === "select" ||
                             /^checkbox|radio$/.test(othis.get(0).type)
                           ) {
                             return othis.next();
                           }
                         }
                         return othis;
                       })(),
                       { tips: 1, tipsMore: true }
                     );
                 }
               );
             if (!layui.device().android && !layui.device().ios) {
               setTimeout(function () {
                 if (debounceFocus()) {
                   othis.get(0).focus();
                 }
               }, 7);
             }
             othis.addClass(LAYUI_DANGER);
             flag = !isTrue;
           }
         }
         return flag;
       });
       return flag;
     },
 
     // 检查一个表单里面的表单元素当前的值是否正常 from  layui.form
     verifyForm(filter) {
       // 表单校验基于layui的form模块，没有就返回true 跳过校验
       if (!layui || !layui.form) return true;
       // layui的form模块是需要使用jQuery的，所以这里暂时可以使用$
       // 寻找parent 如果不传filter，这里需要将this指向VM
       let parent = filter ? $('[lay-filter="' + filter + '"]') : this.parent;
       // 获取表单dom
       let formItem = parent.hasClass("layui-form")
         ? parent
         : parent.find(".layui-form");
       // 获取表单元素集合
       let fieldElem = formItem.find("input,select,textarea");
       let flag = true;
       _.every(fieldElem, function (item) {
         if (item instanceof HTMLElement) {
           flag = FormRender.verifyItem(item);
           return flag;
         }
       });
       return flag;
     },
 
     // 校验并获取一个表单的当前值 from  layui.form
     getFormValue(filter) {
       // 表单校验基于layui的form模块，没有就返回空
       if (!layui || !layui.form) return null;
       // layui的form模块是需要使用jQuery的，所以这里暂时可以使用$
       // 校验,校验如果不通过就返回空
       if (!FormRender.verifyForm.call(this, filter)) return null;
       // 寻找parent 如果不传filter，这里需要将this指向VM
       let parent = filter ? $('[lay-filter="' + filter + '"]') : this.parent;
       // 获取表单dom
       let formItem = parent.hasClass("layui-form")
         ? parent
         : parent.find(".layui-form");
       // 获取表单元素集合,这里是照搬form里面的 getValue 方法
       let fieldElem = formItem.find("input,select,textarea");
       let nameIndex = {};
       let field = {};
       layui.each(fieldElem, function (_, item) {
         item.name = (item.name || "").replace(/^\s*|\s*&/, "");
         if (!item.name) return;
         //用于支持数组 name
         if (/^.*\[\]$/.test(item.name)) {
           var key = item.name.match(/^(.*)\[\]$/g)[0];
           nameIndex[key] = nameIndex[key] | 0;
           item.name = item.name.replace(
             /^(.*)\[\]$/,
             "$1[" + nameIndex[key]++ + "]"
           );
         }
         if (/^checkbox|radio$/.test(item.type) && !item.checked) return;
         field[item.name] = item.value;
       });
       return field;
     },
   };
 
   /**
    * @constructor  虚拟dom节点
    *
    * @param {*} tagName 标签名称,文本节点是使用 TEXT
    * @param {*} nodeType 节点类型
    * @param {*} self 当前的vm作用域
    * HISTORY:                                            *
    *     @Author: Malphite                                 *
    *     @Date: 2022-10-29
    *     @since: v4.0.0
    */
   var vDom = function (tagName, nodeType, self) {
     // 当前的vm作用域
     this.self = self;
     // 标签名称，文本节点使用 TEXT
     this.tagName = tagName;
     // 节点类型，普通节点 1 文本节点 3
     this.nodeType = nodeType;
     // 子节点 <vDom> , 默认就有子节点，到时候直接就去放即可
     this.children = [];
     // 属性值 key - value <vAttr> 文本值以 content 值存入
     this.attrs = {};
   };
 
   /**
    * @constructor  虚拟属性节点
    * @param {*} expression 属性值表达式
    *    字符串或者模版表达式
    * HISTORY:                                            *
    *     @Author: Malphite                                 *
    *     @Date: 2022-10-29
    *     @since: v4.0.0
    */
   var vAttr = function (expression) {
     this.expression = expression;
     this.value = null;
     this.currentValue = null;
   };
 
   /**
    * 组件对象
    * @param {*} options
    * @returns
    */
   var Component = function (options) {
     this.create = function (param) {
       return binder(_.assign({}, options, param));
     };
     return this;
   };
 
   /**
    * @constructor
    * @param {*} options
    * @returns
    */
   var binder = function (options) {
     return new binder.fn.VM(options);
   };
 
   binder.prototype = binder.fn = {
     VM: function (options) {
       this.proxy = new proxy();
       // 初始化生命周期函数
       this.proxy.initEvents.call(this, options);
       // 执行生命周期函数
       this[constant.BEFORE_CREATE_KEY] &&
         this[constant.BEFORE_CREATE_KEY].apply(this);
       // 初始化data
       this.proxy.initData.call(this, options.data, options.dataSource);
       // 初始化计算属性
       this.proxy.initComputed.call(this, options.computeds);
       // 初始化方法
       this.proxy.initMethods.call(this, options.methods);
       // 初始化指令
       this.proxy.initDiretives.call(this, options.diretives);
       // 初始化监视属性
       this.proxy.initWatchs.call(this, options.watchs);
       // 初始化组件
       this.proxy.initComponents.call(this, options.components);
       // 执行生命周期函数
       this[constant.CREATED_KEY] && this[constant.CREATED_KEY].apply(this);
       // 初始化模板
       if (options.template) this.template = options.template;
       if (options.el) this.proxy.mount.call(this, options.el);
       this.isAlive = true;
       return this;
     },
 
     /**
      * 给vm下面的对象设置属性
      * @param {*} obj
      * @param {*} k
      * @param {*} v
      * @returns
      */
     $set(obj, k, v) {
       return this.proxy.set.call(this, obj, k, v);
     },
     /**
      * 添加监视属性
      * @param {*} key
      * @param {*} value
      * @returns
      */
     $watch(key, value) {
       return this.proxy.watch.call(this, key, value);
     },
     /**
      * html挂载
      * @param {*} destination
      */
     $mount(destination) {
       this.proxy.mount.call(this, destination);
     },
     /**
      * 销毁实例
      */
     $destroy() {
       // 执行生命周期函数
       this[constant.BEFORE_DESTROY_KEY] &&
         this[constant.BEFORE_DESTROY_KEY].apply(this);
       _.each(this.$children, (v) => v.$destroy());
       this.isAlive = false;
       this.proxy = null;
       // 执行生命周期函数
       this[constant.DESTROYED_KEY] && this[constant.DESTROYED_KEY].apply(this);
     },
   };
   binder.fn.VM.prototype = binder.fn;
   binder.Method = (k, fn) => (POOL.COMMON_METHODS[k] = fn);
   binder.extend = function (option) {
     let component = new Component(option);
     if (option.name) POOL.COMMON_EXTENDS[option.name] = component;
     return component;
   };
   binder.diretive = (k, fn) => {
     let _diretive = _.isFunction(fn)
       ? new RENDERER.directive(fn)
       : new RENDERER.directive(fn.bind, fn.update);
     POOL.COMMON_DIRETIVES[k] = _diretive;
   };
   return binder;
 });
 
 /**
  * 声明binder下面的组件
  * 将它们声明到layui下面，以便于后面的调用
  */
 function importModule() {
   var doc = window.document;
   var jsPath = doc.currentScript
     ? doc.currentScript.src
     : (function () {
         var js = doc.scripts,
           last = js.length - 1,
           src;
         for (var i = last; i > 0; i--) {
           if (js[i].readyState === "interactive") {
             src = js[i].src;
             break;
           }
         }
         return src || js[last].src;
       })();
   var basePath = jsPath.substring(0, jsPath.lastIndexOf("/") + 1);
   layui.extend({
     windows: basePath + "windows",
     vform: basePath + "vform",
   });
 }
 
 /**
  * @global
  * @description
  *
  *    如果没有监测到引入了lodash，但是实际需要使用它里面的好些方法。这里将这些方法补全，防止报错。
  *    但是会覆盖 _ 这个命名。
  */
 function buildLodash() {
   const handler = {
     /**
      * @method  类型判断： 判断传入的参数是不是数组类型
      * @param {*} o    待判断的对象
      * @returns  true  是数组对象   false  不是数组对象
      */
     isArray: (o) => Object.prototype.toString.call(o) === "[object Array]",
     /**
      * @method  类型判断： 判断传入的参数是不是String类型
      * @param {*} o 待判断的对象
      * @returns  true  是String对象   false  不是String对象
      */
     isString: (o) => Object.prototype.toString.call(o) === "[object String]",
     /**
      * @method  类型判断： 判断传入的参数是不是Boolean类型
      * @param {*} o 待判断的对象
      * @returns  true  是Boolean对象   false  不是Boolean对象
      */
     isBoolean: (o) => Object.prototype.toString.call(o) === "[object Boolean]",
     /**
      * @method  类型判断： 判断传入的参数是不是Function类型
      * @param {*} o 待判断的对象
      * @returns  true  是Function对象   false  不是Function对象
      */
     isFunction: (o) =>
       Object.prototype.toString.call(o) === "[object Function]",
     /**
      * @method  类型判断： 判断传入的参数是不是Date类型
      * @param {*} o 待判断的对象
      * @returns  true  是Date对象   false  不是Date对象
      */
     isDate: (o) => Object.prototype.toString.call(o) === "[object Date]",
     /**
      * @method  类型判断： 判断传入的参数是不是Object类型
      * @param {*} o 待判断的对象
      * @returns  true  是Date对象   false  不是Date对象
      */
     isObject: (o) => Object.prototype.toString.call(o) === "[object Object]",
     /**
      * @method  类型判断： 判断传入的参数是不是RegExp类型
      * @param {*} o 待判断的对象
      * @returns  true  是RegExp对象   false  不是RegExp对象
      */
     isRegExp: (o) => Object.prototype.toString.call(o) === "[object RegExp]",
     /**
      * @method  深拷贝
      * @param {*} o
      */
     cloneDeep: function (o) {
       var res;
       switch (typeof o) {
         case "undefined":
           break;
         case "string":
           res = o + "";
           break;
         case "boolean":
           res = !!o;
           break;
         case "number":
           res = o + 0;
           break;
         case "object":
           if (o == null) {
             res = null;
           } else {
             if (handler.isArray(o)) {
               res = [];
               handler.each(o, (v) => res.push(handler.cloneDeep(v)));
             } else if (handler.isDate(o)) {
               res = new Date();
               res.setTime(o.getTime());
             } else if (handler.isObject(o)) {
               res = {};
               handler.each(o, (v, k) => {
                 res[k] = handler.cloneDeep(v);
               });
             } else if (handler.isRegExp(o)) {
               res = new RegExp(o);
             } else {
               res = o;
             }
           }
           break;
         default:
           res = o;
           break;
       }
       return res;
     },
     /**
      * 返回 值位于 数组 or 字符串 的下标
      * @param {*} a  数组 or 字符串
      * @param {*} v  待判断的值, 可以是单个字符，也可以是一个字符串
      * @returns      下标
      */
     indexOf: (a, v) => {
       let index = -1;
       handler.every(a, (a1, i) => {
         if (a1 == v) {
           index = i;
           return false;
         }
       });
       return index;
     },
     /**
      * 遍历数组 or 对象(这个回调函数的返回值对遍历没得影响)
      * @param {*} o   数组 or 对象
      * @param {*} cb  回调函数
      *   arg0   遍历值
      *   arg1   下标
      *   arg2   数组or对象
      */
     each: (o, cb) => {
       let key;
       //优先处理数组结构
       if (handler.isArray(o)) {
         for (key = 0; key < o.length; key++) {
           cb && handler.isFunction(cb) && cb(o[key], key, o);
         }
       } else {
         for (key in o) {
           cb && handler.isFunction(cb) && cb(o[key], key, o);
         }
       }
     },
     /**
      * 遍历数组 or 对象 直到迭代回调函数返回false 或者 迭代结束
      * 与上面的遍历不同
      * @param {*} o
      * @param {*} cb
      * @returns
      */
     every: (o, cb) => {
       let key;
       //优先处理数组结构
       if (handler.isArray(o)) {
         for (key = 0; key < o.length; key++) {
           if (cb && handler.isFunction(cb)) {
             if (cb(o[key], key, o) === false) break;
           }
         }
       } else {
         for (key in o) {
           if (cb && handler.isFunction(cb)) {
             if (cb(o[key], key, o) === false) break;
           }
         }
       }
     },
     /**
      * 字符串前后去空格
      * @param {*} s
      * @returns
      */
     trim: (s) => $.trim(s),
     /**
      * 参数合并
      * 值得注意的是，与lodash不同 - jq的后面的参数没有值 它就不以最后一个为准了。所以后面的值必须给出默认值''
      */
     assign: (...params) => $.extend.call(this, ...params),
     /**
      * 用标点符号将数组拼接起来
      * @param {*} a  数组
      * @param {*} m  拼接字符
      * @returns
      */
     join: (a, m) => {
       var res = "";
       handler.each(a, (v) => {
         res += String(v) + m;
       });
       if (res != "") res = res.substring(0, res.length - m.length);
       return res;
     },
     /**
      *  数组过滤，返回回调为true的数组。 返回的是一个全新的数组
      * @param {*} o   数组对象
      * @param {*} cb  回调函数
      * @returns
      */
     filter: (o, cb) => {
       var res = [];
       _.each(o, function (v, k) {
         if (cb && handler.isFunction(cb)) {
           if (cb(v, k, o)) res.push(v);
         }
       });
       return res;
     },
     /**
      * 字符串小驼峰,  这里简单处理成全部转成小写字符了
      * @param {*} s  待转化字符
      * @returns      转化后的字符
      */
     camelCase: (s) => String(s).toLowerCase(),
     /**
      * 反转数组,这里只好用原生的代替了
      */
     reverse: (a) => a.reverse(),
     /**
      * 数组排序
      * @param {*} array  待排序数组
      * @param {*} cb     回调函数
      * @returns
      */
     sortBy: (array, cb) => {
       if (!handler.isArray(array)) {
         let temp = [];
         handler.each(array, (v) => temp.push(v));
         array = temp;
       }
       return array.sort(function (a, b) {
         return cb(b) - cb(a);
       });
     },
     /**
      * 遍历数组, 移除回调函数返回为true的项，并将被移除的项返回
      * 也就是说它会修改原来的数组，只保留返回值不为true的。
      * 哪些返回值为true的会封装到一个新的数组里面返回
      * @param {*} o
      * @param {*} cb
      */
     remove: function (o, cb) {
       let res = [];
       handler.each(o, function (v, k, a) {
         if (cb && handler.isFunction(cb)) {
           if (!!cb(v, k, o)) {
             res.push(v);
             a.splice(k, 1);
           }
         }
       });
       return res;
     },
     /**
      * 获取对象的所有key
      */
     keys: (o) => Object.keys(o),
     /**
      * 检查字符串s是否以字符 t 打头
      * @param {*} s
      * @param {*} t
      * @returns
      */
     startsWith: (s, t) => String(s).startsWith(t),
     /**
      * 判断两个对象是否相等
      * @param {*} o0
      * @param {*} o1
      */
     isEqual: (o0, o1) => {
       // 参数有一个不是对象 直接判断值是否相等就行
       if (!handler.isObject(o0) || !handler.isObject(o1)) return o0 === o1;
       // 先比较keys的个数，如果个数不相同 直接就不相等了
       if (handler.keys(o0).length !== handler.keys(o1).length) return false;
       // 以o0为基准 和 o1依次递归比较
       for (let key in o0) {
         const res = handler.isEqual(o0[key], o1[key]);
         if (!res) return false;
       }
       return true;
     },
   };
   return handler;
 }
 
 var INTERVAL_FOCUS_TIME = new Date();
 /**
  * @global
  * @description
  *
  *    通过设置定时的方式防止使表单元素聚焦产生死循环
  *
  * @returns
  */
 function debounceFocus() {
   if (new Date() - INTERVAL_FOCUS_TIME > 2000) {
     INTERVAL_FOCUS_TIME = new Date();
     return true;
   }
   return false;
 }
 
 // 数组特殊处理
 var arrayProto = Array.prototype;
 var arrayMethods = Object.create(arrayProto);
 var methodsToPatch = [
   "push",
   "pop",
   "shift",
   "unshift",
   "splice",
   "sort",
   "reverse",
 ];
 methodsToPatch.forEach(function (method) {
   var original = arrayProto[method];
   Object.defineProperty(arrayMethods, method, {
     value: function () {
       var vm = this.__self__;
       if (!vm) return original.apply(this, arguments);
       var key = this.__key__;
       var proxy = _.cloneDeep(this);
       original.apply(proxy, arguments);
       return vm.proxy.getValue.call(vm, key, proxy);
     },
     enumerable: !!0,
     writable: true,
     configurable: true,
   });
 });
 